package com.eactive.eai.common.authoutbound;

import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.AdapterPropManager;
import com.eactive.eai.adapter.AdapterVO;
import com.eactive.eai.authoutbound.OutboundOAuthCredentialDao;
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
import com.eactive.eai.authoutbound.client.HttpClientAccessTokenServiceByDB;
import com.eactive.eai.authoutbound.client.HttpClientAccessTokenServiceFactoryByDB;
import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.lifecycle.Lifecycle;
import com.eactive.eai.common.lifecycle.LifecycleException;
import com.eactive.eai.common.lifecycle.LifecycleListener;
import com.eactive.eai.common.lifecycle.LifecycleSupport;
import com.eactive.eai.common.session.SessionManager;
import com.eactive.eai.common.session.SessionManagerForIgnite;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger;
import com.mchange.v1.cachedstore.CachedStore.Manager;
import com.openbanking.eai.common.token.AccessTokenVO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.*;
import java.util.function.Function;
import java.util.function.Supplier;

/** 
* 1. 기능 : Access 토큰 정보를 DB로 부터 로딩하고 만료시 재발급 정보를 메모리에 관리하는 Manager 클래스
* 2. 처리 개요 : DB 프로퍼티 정보에 저장된 Access 토큰 정보를 메모리에 로딩하거나 재발급 관리한다.
* *     - 
* 3. 주의사항 
*
* @author  : 
* @version : v 1.0.0
* @see : 관련 기능을 참조
* @since   : 
*  : 
*/
@Component
public class AccessTokenManagerByDB implements Lifecycle {
	/**
	 * Default Logger
	 */
	static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);

	/**
	 * AccessTokenManager Single Instance
	 */
	private static AccessTokenManagerByDB instance = new AccessTokenManagerByDB();

	/**
	 * LifecyleSupport object
	 */
	private LifecycleSupport lifecycle = new LifecycleSupport(this);

	/**
	 * 기동 여부
	 */
	private boolean started; 
	
	/**
	 * 인증 정보를 저장하는 Map
	 */
	private Map<String, OutboundOAuthCredentialVo> outboundOAuthCredentialVos;

	@Autowired
	OutboundOAuthCredentialDao outboundOAuthCredentialDao;

	private ScheduledExecutorService scheduler;
	private Map<String, ScheduledFuture<?>> scheduledTasks;
	private Set<String> runningTasks;
	private static final long TASK_TIMEOUT_SECONDS = 30;

	private static final int TOKEN_SCHEDULER_THREAD_POOL_SIZE = 30;

	public AccessTokenManagerByDB() {
		scheduledTasks = new ConcurrentHashMap<>();
		outboundOAuthCredentialVos = new HashMap<>();
		runningTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
		scheduler = Executors.newScheduledThreadPool(TOKEN_SCHEDULER_THREAD_POOL_SIZE);
	}

	/**
	 * 1. 기능 :  AccessTokenManager Singleton Object를 반환하는 getter method
	 * 2. 처리 개요 : AccessTokenManager Singleton Object를 반환한다.
	 *     -
	 * 3. 주의사항
	 *
	 * @param
	 * @return
	 * @exception
	 **/
	public static AccessTokenManagerByDB getInstance() {
		return ApplicationContextProvider.getContext().getBean(AccessTokenManagerByDB.class);
	}

	/**
	 * 1. 기능 :  AccessTokenManager의 초기화 여부를 반환하는 getter 메서드
	 * 2. 처리 개요 : AccessTokenManager의 초기화 여부를 반환한다.
	 * 3. 주의사항
	 *
	 * @return   초기화 여부
	 **/
	public boolean isStarted() {
		return this.started;
	}

	public void start() throws LifecycleException {
		if (started)
			throw new LifecycleException("RECEAICPM201");

		lifecycle.fireLifecycleEvent(STARTING_EVENT, this);

		try {
			init();
		} catch(Exception e) {
			throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICPM201"));
		}

		started = true;
		if (logger.isWarn()){
			logger.warn("Service started successfully");
		}
		lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
	}

	private void init() throws Exception {
		List<OutboundOAuthCredentialVo> OutboundOAuthCredentialVos = outboundOAuthCredentialDao.getAllOutboundOAuthCredentials();
		logger.info("Initializing with {} credential configurations", OutboundOAuthCredentialVos.size());

		for (OutboundOAuthCredentialVo outboundOAuthCredentialVo : OutboundOAuthCredentialVos) {
			startToken(outboundOAuthCredentialVo);
			outboundOAuthCredentialVos.put(outboundOAuthCredentialVo.getAdapterGroupName(), outboundOAuthCredentialVo);
		}
	}

	public void stop() throws LifecycleException {
		if (!started)
			throw new LifecycleException("RECEAICPM203");

		lifecycle.fireLifecycleEvent(STOPING_EVENT, this);

		// 모든 스케줄된 작업 중지
		scheduledTasks.forEach((name, future) -> {
			future.cancel(true);
			logger.warn("Task stopped for adapter group: {}", name);
		});
		scheduledTasks.clear();

		// 실행 중인 작업 세트 클리어
		runningTasks.clear();

		// 스케줄러 종료
		scheduler.shutdown();
		try {
			if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
				scheduler.shutdownNow();
				logger.warn("Forced shutdown of scheduler after timeout");
			}
		} catch (InterruptedException e) {
			scheduler.shutdownNow();
			logger.error("Scheduler shutdown interrupted", e);
		}

		started = false;

		if (logger.isWarn()){
			logger.warn("Service stopped successfully");
		}
		lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
	}

	public synchronized void refresh(OutboundOAuthCredentialVo outboundOAuthCredentialVo) throws Exception {
		String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
		logger.info("Starting refresh for adapter group: {}", adapterGroupName);

		// 기존 스케줄러 작업 취소
		stopToken(adapterGroupName);

		// 인증 정보 갱신
		outboundOAuthCredentialVos.put(adapterGroupName, outboundOAuthCredentialVo);

		if("N".equals(outboundOAuthCredentialVo.getUseYn())) {
			logger.info("Token task disabled for adapter group: {}", adapterGroupName);
			return;
		}

		// Y인 경우 새로운 스케줄러 작업 시작
		logger.info("Restarting token task for adapter group: {}", adapterGroupName);
		startToken(outboundOAuthCredentialVo);
	}

	public void startToken(final OutboundOAuthCredentialVo outboundOAuthCredentialVo) {
		if (!"Y".equals(outboundOAuthCredentialVo.getUseYn())) {
			logger.debug("Token task not started - disabled for adapter group: {}",
					outboundOAuthCredentialVo.getAdapterGroupName());
			return;
		}

		final String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
		if (scheduledTasks.containsKey(adapterGroupName)) {
			logger.warn("Token task not started - already scheduled for adapter group: {}", adapterGroupName);
			return;
		}

		long interval = outboundOAuthCredentialVo.getIntervalSec() > 0 ?
				outboundOAuthCredentialVo.getIntervalSec() : 24 * 60 * 60;

		logger.info("Starting token task for adapter group: {} with interval: {} seconds",
				adapterGroupName, interval);

		Runnable periodicTask = () -> executeTokenTask(adapterGroupName, outboundOAuthCredentialVo);

		ScheduledFuture<?> scheduledTask = scheduler.scheduleAtFixedRate(
				periodicTask,
				0,  // 즉시 첫 실행
				interval,
				TimeUnit.SECONDS
		);

		scheduledTasks.put(adapterGroupName, scheduledTask);
	}

	private void executeTokenTask(String adapterGroupName, OutboundOAuthCredentialVo credential) {
		if (!runningTasks.add(adapterGroupName)) {
			logger.info("Task skipped - already running for adapter group: {}", adapterGroupName);
			return;
		}

		logger.debug("Starting token task for adapter group: {}", adapterGroupName);
		try {
			Future<?> future = scheduler.submit(() -> {
				try {
					logger.debug("Executing token issuance for adapter group: {}", adapterGroupName);

					SessionManager.getInstance().getOutboundAccessToken(adapterGroupName, new Function<AccessTokenVO, AccessTokenVO>() {

						@Override
						public AccessTokenVO apply(AccessTokenVO accessToken) {
							long intervalTime = System.currentTimeMillis() + (credential.getIntervalSec() * 1000);
	//						// 토큰이 없거나, 다음 스케줄 시간 전에 만료될 경우 재발급
							if (accessToken == null) {
								logger.debug("Token not exists for adapter group: {}", adapterGroupName);
								return issueToken(credential);
							} else if(accessToken.getExpiration() != null) {
								// 토큰이 있고 만료시간이 설정 된 경우
								if(accessToken.getExpiration().before(new Date(intervalTime))){
									// 다음 스케줄 전에 토큰이 만료되는 경우 재발급
									logger.debug("Token expired : {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
									return issueToken(credential);
								} else {
									// 토큰이 아직 유효한 경우
									logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
									return accessToken;
								}
							} else {
								//토큰이 있지만 만료 시간이 없는 경우
								logger.debug("Token exists but expiration time is null for adapter group: {}", adapterGroupName);
								return accessToken;
							}
						}
					});

					logger.debug("Token issuance completed for adapter group: {}", adapterGroupName);
				} catch (Exception e) {
					logger.error("Token issuance failed for adapter group: {}", adapterGroupName, e);
				}
			});

			try {
				future.get(TASK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
			} catch (TimeoutException e) {
				logger.error("Task timed out for adapter group: {}", adapterGroupName);
				future.cancel(true);
			} catch (Exception e) {
				logger.error("Task execution failed for adapter group: {}", adapterGroupName, e);
			}
		} finally {
			runningTasks.remove(adapterGroupName);
			logger.debug("Token task completed for adapter group: {}", adapterGroupName);
		}
	}

	private AccessTokenVO issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo) {
	    String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
	    AccessTokenVO accessToken = null;
	    try {
		
//		boolean isExpired = true;
		
//		if (tokens.containsKey(adapterGroupName)) {
//			AccessTokenVO accessToken = tokens.get(adapterGroupName);
//			isExpired = accessToken.isExpired();
//		}
//		logger.debug("Token status for adapter group: {}, expired: {}, isTokenExpiredWithinInterval: {}", adapterGroupName, isExpired, isTokenExpiredWithinInterval);

//		if (isExpired || isTokenExpiredWithinInterval) {
		
		
			logger.info("Issuing new token for adapter group: {}", adapterGroupName);
//			tokens.remove(adapterGroupName);

			AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);

			if (gvo != null && gvo.getAdapters().hasNext()) {
				AdapterVO avo = gvo.getAdapters().next();
				Properties properties = AdapterPropManager.getInstance().getProperties(avo.getPropGroupName());
				String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");

				HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
				if(service != null) {
					logger.debug("Executing token service of type: {} for adapter group: {}", type, adapterGroupName);
					accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo);
					logger.info("New token issued successfully for adapter group: {}", adapterGroupName);
					return accessToken;
					
				} else {
					logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName);
				}
			} else {
				logger.warn("No valid adapter configuration found for adapter group: {}", adapterGroupName);
			}
		
		} catch (Exception e) {
			logger.error("Task execution failed for adapter group: {}", adapterGroupName, e);
		}

		return accessToken;

	}

	public void stopToken(String adapterGroupName) {
		ScheduledFuture<?> task = scheduledTasks.remove(adapterGroupName);
		if (task != null) {
			task.cancel(true);
			logger.warn("Token task stopped for adapter group: {}", adapterGroupName);
		}
		SessionManager.getInstance().removeOutboundAccessToken(adapterGroupName);
		logger.debug("Token removed for adapter group: {}", adapterGroupName);
	}

	public void refresh(String adapterGroupName) throws Exception {
		refresh(outboundOAuthCredentialDao.getOutboundOAuthCredential(adapterGroupName));
	}

    /**
    * 1. 기능 :  LifecycleListener를 등록하는 메서드
    * 2. 처리 개요 : LifecycleListener를 등록한다.
    * 3. 주의사항 
    *
    * @param    listener    LifecycleEvent를 수신한 LifecycleListener
    **/
    public void addLifecycleListener(LifecycleListener listener)
    {
        lifecycle.addLifecycleListener(listener);
    }

    /** 
    * 1. 기능 :  등록된 LifecycleListener 리스트를 반환하는 메서드
    * 2. 처리 개요 : 등록된 LifecycleListener 리스트를 반환하다.
    * 3. 주의사항 
    *
    * @return   등록된 LifecycleListener 리스트
    **/
    public LifecycleListener[] findLifecycleListeners()
    {
        return lifecycle.findLifecycleListeners();
    }

    /** 
    * 1. 기능 :  등록된 LifecycleListener를 삭제하는 메서드
    * 2. 처리 개요 : 파라미터의 LifecycleListener를 삭제한다.
    * 3. 주의사항 
    *
    * @param    listener    삭제할 LifecycleListener
    **/
    public void removeLifecycleListener(LifecycleListener listener)
    {
        lifecycle.removeLifecycleListener(listener);
    }

	public boolean isOAuthCredentialRegistered(String adapterGroupName){
		OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
		return outboundOAuthCredentialVo != null && "Y".equals(outboundOAuthCredentialVo.getUseYn());
	}

    /** 
    * 1. 기능 :  Access 토큰 정보를 반환하는 메서드
    * 2. 처리 개요 : Access 토큰 정보를 반환한다.
    *     - 
    * 3. 주의사항 
    *
    * @return   Access 토큰 정보
    **/
    public AccessTokenVO getAccessTokenVO(String adapterGroupName) {
	AccessTokenVO accessToken = null;

	try {
	    accessToken = SessionManager.getInstance().getOutboundAccessToken(adapterGroupName,
		    new Function<AccessTokenVO, AccessTokenVO>() {

			@Override
			public AccessTokenVO apply(AccessTokenVO t) {

			    OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos
				    .get(adapterGroupName);

			    return issueToken(outboundOAuthCredentialVo);
			}
		    });

	} catch (Exception e) {
	    if (logger.isError())
		logger.error("토큰을 찾을 수 없습니다. [" + adapterGroupName + "] ", e);
	}

	return accessToken;
    }
    
    /** 
    * 1. 기능 :  Access 토큰 값를 반환하는 메서드
    * 2. 처리 개요 : Access 토큰 값를 반환한다.
    *     - 
    * 3. 주의사항 
    *
    * @return   accessToken 값
    **/
    public String getAccessToken(String adapterGroupName) {
    	String accessToken = null;
	
    	try {
    		accessToken = getAccessTokenVO(adapterGroupName).getAccessToken();   
    	} catch(Exception e) {
    		if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e);
    	}
    	
        return accessToken;
    }

    
	/** 
    * 1. 기능 :  Access 토큰를 삭제하는 메서드
    * 2. 처리 개요 : Access 토큰를 삭제한다.
    *     - 
    * 3. 주의사항 
    *
    **/
	public void removeAccessTokenVO(String adapterGroupName) {
		// 스케줄러 작업 중지 및 토큰 제거
		stopToken(adapterGroupName);
		// OAuth 인증 정보 제거
		outboundOAuthCredentialVos.remove(adapterGroupName);
	}
    
	/** 
    * 1. 기능 :  Access 토큰를 재발급하는 메서드
    * 2. 처리 개요 : Access 토큰를 재발급한다.
    *     - 
    * 3. 주의사항 
    *
    **/
	public synchronized AccessTokenVO retryAccessTokenVO(String adapterGroupName, Properties properties,
		String oldToken) throws Exception {

	    AccessTokenVO accessToken = getAccessTokenVO(adapterGroupName);

	    // 동시 요청이 발생할 경우 synchronized 처리를 했기때문에 토큰을 다시 한번 체크한다.
	    if (accessToken == null || accessToken.isExpired()
		    || StringUtils.equals(accessToken.getAccessToken(), oldToken)) {

			if (isOAuthCredentialRegistered(adapterGroupName) == false) {
			    throw new Exception(
				    "There is no OAuthCredentialRegistered information, or whether to use it is 'N'.");
			}
			String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");
	
			HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
			OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
			if (service != null) {
			    accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties,
				    outboundOAuthCredentialVo);
			}
	    }

	    return accessToken;
	}
} 
