package com.openbanking.eai.common.token;

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.agent.AgentUtil;
import com.eactive.eai.agent.command.Command;
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.property.PropGroupVO;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
import com.openbanking.eai.agent.token.ReloadAccessTokenCommand;
import com.openbanking.eai.common.token.client.HttpClientAccessTokenService;
import com.openbanking.eai.common.token.client.HttpClientAccessTokenServiceFactory;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;

import java.util.Date;
import java.util.HashMap;
import java.util.Properties;

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

	/**
     * 기동 여부
     */    
    private boolean started;

    /**
     * 토큰 정보를 저장하는 collection
     */
    private HashMap<String, AccessTokenVO> tokens;
    
    /**
     * 토큰 발급 Thread를  관리하는 collection
     */
    private HashMap<String, Thread> threads;
    
    
    /** 
    * 1. 기능 :  Default Constructor
    * 2. 처리 개요 : 
    *     - 
    * 3. 주의사항 
    *
    **/
    private AccessTokenManager() {
    	tokens = new HashMap<String, AccessTokenVO>();
    	threads = new HashMap<String, Thread>();
    }
    
    /** 
    * 1. 기능 :  AccessTokenManager Singleton Object를 반환하는 getter method
    * 2. 처리 개요 : AccessTokenManager Singleton Object를 반환한다.
    *     - 
    * 3. 주의사항 
    *
    * @param 
    * @return 
    * @exception
    **/
    public static AccessTokenManager getInstance() {
        return instance;
    }
    
    /** 
    * 1. 기능 :  AccessTokenManager의 초기화 여부를 반환하는 getter 메서드
    * 2. 처리 개요 : AccessTokenManager의 초기화 여부를 반환한다.
    * 3. 주의사항 
    *
    * @return   초기화 여부
    **/
    public boolean isStarted() {
        return this.started;   
    }
    
    // =================================================================================
    // 이 부분은 Lifecycle용으로 제공되는 메소드이다.
    /** 
    * 1. 기능 :  Lifecycle의 start 메서드로 AccessTokenManager를 초기화하는 메서드
    * 2. 처리 개요 : PropertyDAO를 이용해 추출 Rule 정보 모두를 가져와 초기화한다. <---
    *     - 
    * 3. 주의사항 
    *
    * @param 
    * @return 
    * @exception
    **/
    public void start() throws LifecycleException {
        if (started)
            throw new LifecycleException("RECEAICPM201");

        // Notify our interested LifecycleListeners
        lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
        
        try {
        	// accessToken 초기화
        	tokens.clear(); 
        	
        	init();
        } catch(Exception e) {
            throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICPM201"));
        }
        
        started = true;
        if (logger.isWarn()){ 
        	logger.warn("AccessTokenManager] It is started.");
        }
        // Notify our interested LifecycleListeners
        lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
    }
    
    private void init() throws Exception  {
    	
    	String adapterGroupNameList = getTargetAdapterGroup();
    	String[] adapterGroupNames = adapterGroupNameList.split("[,]");
    	
    	if (adapterGroupNames.length > 0) {
    		// 어댑터그룹별 토큰 관리
    		for (String adapterGroupName : adapterGroupNames) {
    			startToken(adapterGroupName);
    		}
    	}
    }

    /** 
    * 1. 기능 :  Lifecycle의 stop 메서드로 AccessTokenManager를 종료하는 메서드
    * 2. 처리 개요 : 멤버에 캐싱한 Access 토큰 정보를 clear한다.
    *     - 
    * 3. 주의사항 
    *
    * @param 
    * @return 
    * @exception
    **/
    public void stop() throws LifecycleException {
        // Validate and update our current component state
        if (!started)
            throw new LifecycleException("RECEAICPM203");
		
        // Notify our interested LifecycleListeners
        lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
        
        // 기동중인 Thread가 존재하면 전체를 중지 시킨다. 
        if (threads.size() > 0) {
        	threads.forEach((n, t) -> 
        			{
        				try {
        					if (t.isAlive()) {
            					t.stop();
            					
            		        	logger.warn("AccessTokenManager] STOP (" + n + ") stopped.");
            				}
        				} catch (Exception ignore) {
        					;
        				}
        				
        			});
        }
        
        this.tokens = null;
        
        started = false;
        if (logger.isWarn()){
        	logger.warn("AccessTokenManager] It is stopped.");
        }
        // Notify our interested LifecycleListeners
        lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
    }
    
    public void startToken(String adapterGroupName) {
    	
    	// 이미 기동중인 Thread가 존재하면 Skip한다.
    	if (threads.containsKey(adapterGroupName)) {
    		logger.warn("AccessTokenManager] SKIP (" + adapterGroupName + ") thread aleady started");
    		return;
    	}
    	
    	Runnable runnable = new Runnable() {
    		long sleepTime = 60;
    		long startTime = System.currentTimeMillis();
    		
			public void run() {
				while(true) {
					long interval = 24 * 60 * 60;
					AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
			    	// 어댑터가 사용중이 아니면 Skip한다.
			    	if (gvo != null && gvo.isUsedFlag()) {
			    		try {
							PropGroupVO vo = PropManager.getInstance().getPropGroupVO(AccessTokenVO.PROP_GROUP);
							String tokenInfo = vo.getProperty(adapterGroupName);
							
					    	// 토큰 정보가 없을 경우 중지한다.
					    	if (StringUtils.isBlank(tokenInfo)) {
					    		logger.error("AccessTokenManager] INFO (" + adapterGroupName + ") is NULL");
					    		break;
					    	}
					    	
				    		JSONObject tokenInfoObject = (JSONObject) JSONValue.parse(tokenInfo);
					    	// 초단위 입력. 값이 없을 경우 1일(24 * 60 * 60초)
					    	try {
					    		interval = tokenInfoObject.containsKey("interval") ? Long.parseLong((String) tokenInfoObject.get("interval")) : 24 * 60 * 60;
					    	} catch(Exception e) {
					    		logger.warn("AccessTokenManager] SLEEP (" + adapterGroupName + ") default interval");
					    	}
							
					    	long intervalTime = startTime + (interval * 1000L);
							
							// interval이 0보다 작은 경우 Thread를 중지한다.
							if (interval < 0) {
								logger.error("AccessTokenManager] STOP (" + adapterGroupName + ")");
					    		break;
							}
							
					    	// 해당 어댑터그룹명의 토큰이 존재할 경우  
					    	if (!tokens.isEmpty() && tokens.containsKey(adapterGroupName)) {
					    		if (System.currentTimeMillis() > intervalTime) {
					    			long tempTime = System.currentTimeMillis();
					    			AccessTokenVO accessToken = tokens.get(adapterGroupName);
			    					
			    					// 다음 스케줄 시간 전까지 만료가 될경우 재 발급
			    					if (accessToken.getExpiration() != null && accessToken.getExpiration().before(new Date(intervalTime))) {
			    						refresh(adapterGroupName, true);

			    						logger.warn("AccessTokenManager] REFRESH (" + adapterGroupName + ") AccessToken : " + instance.getAccessTokenVO(adapterGroupName));
			    					} else {
		    		    				logger.info("AccessTokenManager] SKIP (" + adapterGroupName + ") AccessToken : " + instance.getAccessTokenVO(adapterGroupName));
			    					}
			    					
			    					startTime = tempTime;
					    		}
	    					// 해당 어댑터그룹명의 토큰이 존재하지 않을 경우  
					    	} else {
					    		long tempTime = System.currentTimeMillis();
					    		
					    		refresh(adapterGroupName);
					    		
					    		startTime = tempTime;
					    		
			    				logger.warn("AccessTokenManager] NEW (" + adapterGroupName + ") AccessToken : " + instance.getAccessTokenVO(adapterGroupName));
					    	}
						} catch(Throwable t) {
							logger.error("AccessTokenManager] FAIL (" + adapterGroupName + ") start failed", t);
						}
			    	} else {
			    		logger.info("AccessTokenManager] SKIP (" + adapterGroupName + ") adapter not used");
			    	}
					
					try {
			        	logger.info("AccessTokenManager] SLEEP (" + adapterGroupName + ") " + sleepTime + " sec.");

			        	Thread.sleep(sleepTime * 1000L);
					} catch(InterruptedException e) {
						logger.error("AccessTokenManager] SLEEP (" + adapterGroupName + ") failed", e);
					}
				}
			}
		};
		
		Thread thread = new Thread(runnable);
		thread.setName(adapterGroupName + "-AccessTokenRefresh");
		thread.setPriority(Thread.MAX_PRIORITY);
		thread.start();
		
		// Thread 관리를 위함 
		threads.put(adapterGroupName, thread);
    }
    
    public void stopToken(String adapterGroupName) {
    	
    	// 기동중인 Thread가 존재하지 않으면 Skip한다.
    	if (!threads.containsKey(adapterGroupName)) {
    		return;
    	}
    	
    	Thread t = threads.get(adapterGroupName);
    	
    	if (t.isAlive()) {
			t.stop();
			
			if (logger.isDebugEnabled()){
	        	logger.warn("AccessTokenManager] STOP (" + adapterGroupName + ") stopped.");
	        }
			
			threads.remove(adapterGroupName);
			tokens.remove(adapterGroupName);
		}
    }
    
    public String getStatus(String adapterGroupName) {
    	
    	if (!threads.containsKey(adapterGroupName)) {
    		return "STOPED";
    	} else {
    		return "STARTED";
    		
//    		Thread t = threads.get(adapterGroupName);
//    		
//    		if (t.isAlive()) {
//    			return "STARTED";
//    		} else {
//    			return "SLEEP";
//    		}
    	}
    }
    
    /** 
    * 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 String getTargetAdapterGroup() throws Exception {
    	
		PropGroupVO vo = PropManager.getInstance().getPropGroupVO(AccessTokenVO.PROP_GROUP);
		return vo.getProperty(AccessTokenVO.TARGET_ADAPTER_GROUP);
    }
    
    
    public void refresh() throws Exception {
    	if(logger.isWarn()) {
    		logger.warn("AccessTokenManager] refresh all Started ...");
    	}
    	String adapterGroupNameList = getTargetAdapterGroup();
    	String[] adapterGroupNames = adapterGroupNameList.split("[,]");
    	
    	if (adapterGroupNames.length > 0) {
    		// 어댑터별 토큰 관리
    		for (String adapterGroupName : adapterGroupNames) {
    			refresh(adapterGroupName);
    		}
    	}
    	
    	if(logger.isWarn()) {
    		logger.warn("AccessTokenManager] refresh all finished ...");
    	}
    }
    
    public void refresh(String adapterGroupName) throws Exception {
    	refresh(adapterGroupName, false);
    }
    
    public synchronized void refresh(String adapterGroupName, boolean isRefresh) throws Exception {
    	boolean isExpired = true;

    	if (tokens.containsKey(adapterGroupName)) {
    		AccessTokenVO accessToken = tokens.get(adapterGroupName);
    		isExpired = accessToken.isExpired();
    	} 

		// 만료됐거나 토큰 존재하지 않을때
    	if (isExpired || isRefresh) {
			// 기존 토큰이 있을 경우 제거
			tokens.remove(adapterGroupName);
			
			AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
    		
    		if (gvo != null && gvo.getAdapters().hasNext()) {
    			// 어댑터의 첫 프로퍼티 정보를 참조
    			AdapterVO avo = (AdapterVO)gvo.getAdapters().next();
    			
    			Properties properties = AdapterPropManager.getInstance().getProperties(avo.getPropGroupName());
    			
    			String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");

    			// 토큰 발급 서비스가 다를 경우 HttpClientTokenService 인터페이스를 구현한 서비스를 추가
    			HttpClientAccessTokenService service = HttpClientAccessTokenServiceFactory.createFactory(type);
    			if(service != null) {
    				AccessTokenVO accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties);
    				this.tokens.put(adapterGroupName, accessToken);
    			}
    		}
    	}
    }    
    
    /** 
    * 1. 기능 :  모든 Access 토큰을 찾아 반환하는 메서드
    * 2. 처리 개요 : 모든 Access 토큰을 찾아 HashMap 객체로반환한다.
    * 3. 주의사항 
    *
    * @return   Access 토큰 정보 VO 객체 Collection
    **/
    public HashMap<String, AccessTokenVO> getAllAccessTokenVO() {
        return tokens;
    } 
    
    /** 
    * 1. 기능 :  Access 토큰 정보를 반환하는 메서드
    * 2. 처리 개요 : Access 토큰 정보를 반환한다.
    *     - 
    * 3. 주의사항 
    *
    * @return   Access 토큰 정보
    **/
    public AccessTokenVO getAccessTokenVO(String adapterGroupName) {
    	AccessTokenVO accessToken = null;
    	
    	try {
    		accessToken = tokens.get(adapterGroupName);  
    	} 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 = tokens.get(adapterGroupName).getAccessToken();   
    	} catch(Exception e) {
    		if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e);
    	}
    	
        return accessToken;
    }
    
    /** 
     * 1. 기능 :  Access 토큰 정보를 등록하는 메서드
     * 2. 처리 개요 : Access 토큰 정보를 등록한다.
     *     - 
     * 3. 주의사항 
     *
     * @param    vo      AccessTokenVO
     **/
    public void setAllAccessTokenVO(HashMap<String, AccessTokenVO> tokens) {
    	this.tokens = tokens;
    }
    
	/** 
    * 1. 기능 :  Access 토큰 정보를 등록하는 메서드
    * 2. 처리 개요 : Access 토큰 정보를 등록한다.
    *     - 
    * 3. 주의사항 
    *
    * @param    vo      AccessTokenVO
    **/
    public void setAccessTokenVO(String adapterGroupName, AccessTokenVO accessToken) {
    	tokens.put(adapterGroupName, accessToken);
    }
    
	/** 
    * 1. 기능 :  Access 토큰를 삭제하는 메서드
    * 2. 처리 개요 : Access 토큰를 삭제한다.
    *     - 
    * 3. 주의사항 
    *
    **/
    public void removeAccessTokenVO(String adapterGroupName) {
    	tokens.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)) {
	    	String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");
	    	
	    	HttpClientAccessTokenService service = HttpClientAccessTokenServiceFactory.createFactory(type);
	    	if(service != null) {
	    		accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties);
	    	}
	    	tokens.put(adapterGroupName, accessToken);
    	}
    	
	    return accessToken;
    }
    
	/** 
     * 1. 기능 :  Access 토큰를 동기화하는 메서드
     * 2. 처리 개요 : Access 토큰를 동기화한다.
     *     - 
     * 3. 주의사항 
	 * @throws Exception 
     *
     **/
    public synchronized void notifyAllAccessTokenVO() throws Exception {
    	try {
    		Command command = new ReloadAccessTokenCommand();
            command.setArgs(tokens);
            AgentUtil.broadcast(command);
    	} catch (Exception e) {
    		throw new Exception("토큰 동기화에 실패했습니다. " +e.getMessage());
		}
    }
} 
