package com.ktds.act.apigw.api;

import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import com.ktds.act.apigw.api.config.PortalApi;
import com.ktds.act.apigw.api.db.entity.AdmApiDply;
import com.ktds.act.apigw.api.db.service.AdmApiService;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@Endpoint(id = "api")// /info/api로 변경 가능한지
//InitializingBean Container가 뜨기 전에 설정해주는 값.
public class ApiFactory implements InitializingBean {
	private static Map<String, String> uriMap = null; // key -> meth:uriIn, value -> sysId.id
	private static Map<String, AdmApiDply> apiMap = null; // key -> sysId.id, value -> ApiDto
	private static List<String> portalUriList = null;
	private static List<String> portalApiList = null;

	private static LocalDateTime lastModifiedDate = null;
	private static PathMatcher pathMatcher = new AntPathMatcher();

	@Autowired
	private PortalApi portalApi;

	@Autowired
	private AdmApiService admApiService;

	/**
	 * Refresh API configuration
	 * 내부 연동 API는 Application.yaml에서 관리
	 * @param e
	 */
	@EventListener
	public void refresh(RefreshScopeRefreshedEvent e) {
		log.info("Refresh ApiFactory");
		afterPropertiesSet();
	}

	/**
	 * API synchronization
	 */
	@Scheduled(fixedDelayString = "${common.api.schedule.dbSync}", timeUnit = TimeUnit.SECONDS)
	public void synchronizeDB() {

		// 마지막 동기화 시점 시간을 비교하여 데이터 동기화 처리
		LocalDateTime nowTime = LocalDateTime.now();
		List<AdmApiDply> dbApiList = null;

		String apiUri = "";
		String sysIdApiId = "";

		Boolean newApiCheck = false;

		// 이전 DB 동기화 데이터 건수
		int oldTotCnt = apiMap.size();

		// DB 동기화
		if (lastModifiedDate == null) {
			dbApiList = admApiService.getAdmApiList("DPLY");
		} else {
			dbApiList = admApiService.getAdmApiList(lastModifiedDate);
		}

		// // 최신 DB 동기화 데이터 건수
		// int totCnt = (int) apiRepository.count();

		// 추가, 변경분 반영
		if(dbApiList.size() > 0) {
			for (AdmApiDply apiEntity : dbApiList) {
				sysIdApiId = apiEntity.getSysId().concat(".").concat(apiEntity.getApiId()).concat(apiEntity.getVer());
				if("DPLY".equals(apiEntity.getDplyType())){

					if((apiMap.size() > 0) && !apiMap.containsKey(sysIdApiId) && !newApiCheck) {
						newApiCheck = true;
					}

					for(String meth : apiEntity.getMeth()) {
						apiUri = meth.concat(":").concat(apiEntity.getIn());

						// URI 정보 추가
						uriMap.put(apiUri, sysIdApiId);
					}

					for(String uriMapKey : uriMap.keySet()) {
						if(uriMap.get(uriMapKey).equals(sysIdApiId)) {
							if(!apiEntity.getMeth().contains(uriMapKey.split(":")[0])) {

								// 기존 URI 정보 삭제

								uriMap.remove(uriMapKey);

							}
						}
					}

					// API 정보 추가
					apiMap.put(sysIdApiId, apiEntity);

					log.info("ApiFactory.synchronizeDB - Load : " + apiEntity.toString());
				}else{
					for(String meth : apiEntity.getMeth()) {
						apiUri = meth.concat(":").concat(apiEntity.getIn());

						// URI 정보 추가
						uriMap.remove(apiUri);
					}
					apiMap.remove(sysIdApiId);
				}
				
//				// 마지막 동기화 시간 현행화
//				if (lastModifiedDate == null || (apiEntity.getDplyDt() != null && apiEntity.getDplyDt().isAfter(lastModifiedDate))) {
//					lastModifiedDate = nowTime;
//				}
				
			}

			// 마지막 동기화 시간 현행화
			AdmApiDply lastApi = dbApiList.stream().max((o1, o2) -> o1.getDplyDt().compareTo(o2.getDplyDt())).get();
			lastModifiedDate = lastApi.getDplyDt();
		}

		// // DB 삭제분 반영
		// if((oldTotCnt > 0) && ((totCnt < oldTotCnt) || ((totCnt >= oldTotCnt) && newApiCheck))) {
		// 	List<String> sysIdAndApiIdList = apiRepository.findSysIdAndApiId();

		// 	// DB에 기존 정보가 없는 경우
		// 	for(String apiMapKey : apiMap.keySet()) {
		// 		if(!sysIdAndApiIdList.contains(apiMapKey)) {
		// 			for(String meth : apiMap.get(apiMapKey).getMeth()) {
		// 				apiUri = meth.concat(":").concat(apiMap.get(apiMapKey).getUriIn());

		// 				// 기존 URI 정보 삭제
		// 				// portal url에 속하지 않을 때만 적용
		// 				if(!portalUriList.contains(apiUri)){
		// 					uriMap.remove(apiUri);
		// 					log.info("ApiFactory.synchronizeDB - Remove : " + apiMapKey);
		// 				}
		// 			}
		// 			// 기존 API 정보 삭제
		// 			// portal api에 속하지 않을 때만 적용
		// 			if(!portalApiList.contains(apiMapKey)){
		// 				apiMap.remove(apiMapKey);
		// 			}
		// 		}
		// 	}
		// }

	}

	/**
	 * DB, Custom API Loading
	 */
	@Override
	public void afterPropertiesSet() {
		log.info("ApiFactory.afterPropertiesSet - Start");
		uriMap = new ConcurrentHashMap<String, String>();
		apiMap = new ConcurrentHashMap<String, AdmApiDply>();
		portalUriList = new ArrayList<>();
		portalApiList = new ArrayList<>();

		// YAML에 정의된 API 동기화
		var portalApiDtos = portalApi.getApi();
		String sysIdApiId="";
		String apiUri="";
		for(AdmApiDply portalApi: portalApiDtos){
			sysIdApiId = portalApi.getSysId().concat(".").concat(portalApi.getApiId()).concat(portalApi.getVer());
			for(String meth : portalApi.getMeth()) {
				apiUri = meth.concat(":").concat(portalApi.getIn());
				uriMap.put(apiUri, sysIdApiId);
				portalUriList.add(apiUri);
			}
			apiMap.put(sysIdApiId, portalApi);
			portalApiList.add(sysIdApiId);
			log.info("Portal Api synchronization : " + portalApi.toString());
		} // db에 해당 정보 없어서 밑에 메소드 실행 시 삭제되어버림 => 해당 사항들만 삭제 안되도록 셋팅 필요
		synchronizeDB();

		// Custom API 동기화
		// for (ApiInfo apiVo : customApiList.getApiList()) {
		// 	for (String method : apiVo.getMeth().split("/")) {
		// 		String apiUri = method.concat(":").concat(apiVo.getUriIn());
		// 		apiMap.put(apiUri, apiVo);
		// 	}
		// }
		log.info("ApiFactory.afterPropertiesSet - End");
	}

	/**
	 * API search by method and URI
	 *
	 * @param method
	 * @param uri
	 * @return api
	 * **API에 대한 식별 uriIn을 매핑해주는 부분**
	 */
	public static AdmApiDply getApi(String method, String uri) {
		AdmApiDply api = null;
		String apiUri = method.concat(":").concat(uri);
		try{
			
		if (uriMap.containsKey(apiUri)) {
			api = apiMap.get(uriMap.get(apiUri));
		} else {
			//이 부분에 대해서 path param
			List<String> matchingPatterns = new ArrayList<>();
			for (String key : uriMap.keySet()) {
				String apiPattern = key.split(":")[1];
				String apiMethod = key.split(":")[0];
				if(checkAPISimilarityWithMethodAndUri(method, uri, apiPattern, apiMethod)){
					matchingPatterns.add(apiPattern);
				}
			}
			String bestMatch = findBestMatchPattern(uri, matchingPatterns);
			api = findBestMatchApi(method, uri, api, bestMatch);
		}
		}catch(Exception e) {
			e.printStackTrace();
		}
		return api;
	}

	public static AdmApiDply getApiByApiId(String apiId) {
		AdmApiDply api = null;

		for (String key : apiMap.keySet()) {
			String[] SysIdNApiId = key.split("\\.");
			if (SysIdNApiId[1].equals(apiId)) {
				api = apiMap.get(key);
				break;
			}
		}

		return api;
	}

	private static AdmApiDply findBestMatchApi(String method, String uri, AdmApiDply api, String bestMatch) {
		if (bestMatch != null) {
			api = apiMap.get(uriMap.get(method.concat(":").concat(bestMatch)));

			if (api == null) {
				if (bestMatch.endsWith("/")) {
					api = apiMap.get(uriMap.get(method.concat(":").concat(bestMatch.substring(0, bestMatch.length() - 1))));
				}
				if (api == null) {
					throw new IllegalStateException(
							"Could not find handler for best pattern match [" + bestMatch + "]");
				}
			}
		}
		return api;
	}

	/**
	 * matchingPatterns에 저장된 패턴을 탐색하면서 현재 uri와 가장 적합한 패턴을 찾아 반환한다.
	 *
	 *
	 * @param uri
	 * @param matchingPatterns
	 * @return String
	 */
	private static String findBestMatchPattern(String uri, List<String> matchingPatterns) {
		String bestMatch=null;
		// comparator 기준 : 더 구체적인지 여부. 패턴이 비슷한지 여부 x. 패턴 비슷한 지 여부는 아래 match에서 확인
		Comparator<String> patternComparator = pathMatcher.getPatternComparator(uri);
		if (!matchingPatterns.isEmpty()) {
			matchingPatterns.sort(patternComparator);
			bestMatch = matchingPatterns.get(0);
		}
		return bestMatch;
	}

	/**
	 * 호출된 api의 method와 uriMap에 저장된 method 정보가 같고,
	 * uriMap에 저장된 uri 패턴에 호출된 api의 uri가 포함된다면 true를 반환한다.
	 *
	 * @param method
	 * @param uri
	 * @param apiPattern
	 * @param apiMethod
	 * @return api
	 */
	private static boolean checkAPISimilarityWithMethodAndUri(String method, String uri, String apiPattern, String apiMethod) {

		//uriMap에 저장된 uri가 /* 으로 끝나는 패턴일 경우, * 하나 더 붙여준다(AntPathMatcher 패턴 처리를 맞추기 위해)
		if(apiPattern.endsWith("/*")) {
			apiPattern = apiPattern.concat("*");
		}

		return pathMatcher.match(apiPattern, uri) && apiMethod.equals(method);
	}

	
	@ReadOperation
    public Map<String, AdmApiDply> apiFeatures() {
        return apiMap;
    }

}

handlerFactory → 컴파일해서 스프링 컨테이너에 올리면 게이트웨이 입장에서는 그냥 가져다 쓰게 되는 것이다.

HandlerFactory → bodyrefboolmap 이걸 하느냐 안하느냐에 따라서 소도차이가 많이 난다.

핸들러에서 → DPLY가지고만 동기화하다가 스케줄링 돌릴때는 DPLY외에는 모두 지우게 된다. 스프링 컨테이너에서 삭제

응답에서 바디를 무조건 식별할수 있어야 한다? 구체적으로? → 식별

→ 그럴려면 앞에 무조건 nettyWriteResponseFilter → order -1