package com.eactive.eai.rms.onl.apim.obp;

import com.eactive.apim.portal.apprequest.entity.AppRequest;
import com.eactive.apim.portal.apprequest.repository.AppRequestRepository;
import com.eactive.apim.portal.obp.entity.ObpGwMetric;
import com.eactive.eai.rms.common.base.BaseService;
import com.eactive.eai.rms.common.datasource.DataSourceContextHolder;
import com.eactive.eai.rms.common.datasource.DataSourceTypeManager;
import com.eactive.eai.rms.common.util.CommonUtil;
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricJpaDAO;
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricService;
import com.eactive.eai.rms.data.entity.onl.apim.obp.ObpGwMetricVO;
//import com.eactive.ext.kjb.common.KjbPropertyHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * ObpGwMetric Man Service - 비즈니스 로직
 * API Gateway 로그 데이터를 집계하여 ObpGwMetric 엔티티로 저장
 */
@Service
@Transactional(transactionManager = "transactionManagerForEMS")
public class ObpGwMetricManService extends BaseService {

    private static final Logger log = LoggerFactory.getLogger(ObpGwMetricManService.class);

    /**
     * 기본 집계 범위 시간 (단위: 시간)
     * - 1: 직전 1시간만 집계
     * - 5: 직전 5시간까지 각각 1시간 단위로 집계
     *
     * <p>Job 파라미터로 오버라이드 가능 (aggregation.hour.range)</p>
     */
    public static final int DEFAULT_AGGREGATION_HOUR_RANGE = 1;

    private static final DateTimeFormatter TIMESLICE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHH0000000");
    private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
    private static final DateTimeFormatter YYYYMMDD_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

    private final ObpGwMetricJpaDAO obpGwMetricJpaDAO;
    private final ObpGwMetricService obpGwMetricService;
    private final AppRequestRepository appRequestRepository;

    public ObpGwMetricManService(ObpGwMetricJpaDAO obpGwMetricJpaDAO,
                                  ObpGwMetricService obpGwMetricService,
                                  AppRequestRepository appRequestRepository) {
        this.obpGwMetricJpaDAO = obpGwMetricJpaDAO;
        this.obpGwMetricService = obpGwMetricService;
        this.appRequestRepository = appRequestRepository;
    }

    /**
     * 다중 시간대 집계 실행 (Job에서 호출) - 기본 집계 범위 사용
     */
    public void executeHourlyAggregation() {
        executeHourlyAggregation(DEFAULT_AGGREGATION_HOUR_RANGE);
    }

    /**
     * 다중 시간대 집계 실행 (Job에서 호출)
     *
     * @param aggregationHourRange 집계 범위 시간 (단위: 시간)
     *                             예: 1=직전 1시간, 3=직전 3시간 각각 집계
     */
    public void executeHourlyAggregation(int aggregationHourRange) {
        long totalStartTime = System.currentTimeMillis();
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime baseHour = now.truncatedTo(ChronoUnit.HOURS);  // 현재 시간의 정각

        log.info("========== ObpGwMetric 집계 시작 ==========");
        log.info("실행 시간: {}, aggregationHourRange: {}", now, aggregationHourRange);

        for (int i = 1; i <= aggregationHourRange; i++) {
            LocalDateTime targetHour = baseHour.minusHours(i);
            log.info("집계 대상 시간대: {}", targetHour);
            createHourlyData(targetHour);
        }

        long totalElapsed = System.currentTimeMillis() - totalStartTime;
        log.info("========== ObpGwMetric 집계 완료 ==========");
        log.info("전체 집계 소요시간: {}ms ({}초)", totalElapsed, String.format("%.2f", totalElapsed / 1000.0));
    }

    /**
     * 1시간 단위 ObpGwMetric 데이터 생성 (UPSERT)
     */
    public void createHourlyData(LocalDateTime targetHour) {
        long jobStartTime = System.currentTimeMillis();
        log.info("ObpGwMetric 집계 시작: targetHour={}", targetHour);

        try {
            String timeslice = targetHour.format(TIMESLICE_FORMATTER);
            String yyyymmdd = targetHour.format(YYYYMMDD_FORMATTER);
            String tableName = CommonUtil.getLogTable(yyyymmdd, true);
            String startTime = targetHour.format(TIMESTAMP_FORMATTER);
            String endTime = targetHour.plusHours(1).minusNanos(1).format(TIMESTAMP_FORMATTER);

            // ═══════════════════════════════════════════════════════════════════
            // 1단계: GW DB에서 EAIBZWKDSTCD 코드 목록 조회
            // ═══════════════════════════════════════════════════════════════════
            long step1Start = System.currentTimeMillis();
            DataSourceContextHolder.setDataSourceType(
                    DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.APIGW));

            List<String> bzwkCodes = obpGwMetricJpaDAO.selectAllBzwkCodes();
            log.info("[1단계] EAIBZWKDSTCD 코드 조회 완료: count={}, elapsed={}ms",
                    bzwkCodes.size(), System.currentTimeMillis() - step1Start);

            // ═══════════════════════════════════════════════════════════════════
            // 2단계: 코드별 개별 쿼리 실행 후 결과 병합
            // ═══════════════════════════════════════════════════════════════════
            long step2Start = System.currentTimeMillis();
            Map<String, ObpGwMetricVO> aggregatedMap = new HashMap<>();
            int totalLogCount = 0;

            for (String bzwkCode : bzwkCodes) {
                long codeStart = System.currentTimeMillis();

                List<ObpGwMetricVO> partialResult = obpGwMetricJpaDAO.selectHourlyMetricsByBzwkCode(
                        tableName, bzwkCode, startTime, endTime, timeslice);

                for (ObpGwMetricVO vo : partialResult) {
                    String key = vo.getAggregationKey();
                    aggregatedMap.merge(key, vo, this::mergeMetricVO);
                }

                if (!partialResult.isEmpty()) {
                    log.debug("[2단계] bzwkCode={}, 집계건수={}, elapsed={}ms",
                            bzwkCode, partialResult.size(), System.currentTimeMillis() - codeStart);
                }
                totalLogCount += partialResult.size();
            }

            List<ObpGwMetricVO> aggregatedList = new ArrayList<>(aggregatedMap.values());
            log.info("[2단계] 로그 집계 완료: 원본건수={}, 병합후건수={}, elapsed={}ms",
                    totalLogCount, aggregatedList.size(), System.currentTimeMillis() - step2Start);

            if (aggregatedList.isEmpty()) {
                log.warn("집계 대상 데이터 없음: targetHour={}", targetHour);
                return;
            }

            // ═══════════════════════════════════════════════════════════════════
            // 3단계: 부가 정보 조회 (apiName, uri) - GW DB
            // ═══════════════════════════════════════════════════════════════════
            long step3Start = System.currentTimeMillis();
            Set<String> apiIds = aggregatedList.stream()
                    .map(ObpGwMetricVO::getApiId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());

            // URI 조회용 EAI 서비스 일련번호 (HTTP_ADAPTER_EXTRA_LOG.GUID와 매핑)
            Set<String> eaiSvcSernos = aggregatedList.stream()
                    .map(ObpGwMetricVO::getEaiSvcSerno)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());

            Map<String, String> apiNameMap = obpGwMetricJpaDAO.selectApiNames(apiIds);
            ObpGwMetricJpaDAO.UriMethodResult uriMethodResult = obpGwMetricJpaDAO.selectUrisAndMethods(eaiSvcSernos);
            Map<String, String> uriMap = uriMethodResult.getUriMap();
            Map<String, String> methodMap = uriMethodResult.getMethodMap();
            log.info("[3단계] 부가정보 조회 완료: apiCount={}, eaiSvcSernoCount={}, elapsed={}ms",
                    apiIds.size(), eaiSvcSernos.size(), System.currentTimeMillis() - step3Start);

            // ═══════════════════════════════════════════════════════════════════
            // 4단계: 지표 DB로 전환 후 appId 조회 및 UPSERT
            // ═══════════════════════════════════════════════════════════════════
            long step4Start = System.currentTimeMillis();
            DataSourceContextHolder.setDataSourceType(
                    DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING));

            Set<String> orgIds = aggregatedList.stream()
                    .map(ObpGwMetricVO::getOrgId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());
            // AppRequestRepository(@EMSDataSource)를 사용하여 MONITORING 스키마에서 조회
            Map<String, String> appIdMap = selectAppIdsByOrgIds(orgIds);

            int insertCount = 0;
            int updateCount = 0;
            int conflictRetryCount = 0;

            for (ObpGwMetricVO vo : aggregatedList) {
                LocalDateTime timesliceDateTime = vo.getTimesliceAsLocalDateTime();

                try {
                    Optional<ObpGwMetric> existing = obpGwMetricService.findByUniqueKey(
                            timesliceDateTime,
                            vo.getApiId(),
                            vo.getClientId(),
                            vo.getHostname(),
                            vo.getOrgId()
                    );

                    if (existing.isPresent()) {
                        // UPDATE: 기존 데이터 갱신
                        updateExistingMetric(existing.get(), vo, apiNameMap, uriMap, methodMap, appIdMap);
                        updateCount++;
                    } else {
                        // INSERT: 신규 데이터 생성
                        ObpGwMetric entity = convertToEntity(vo, apiNameMap, uriMap, methodMap, appIdMap);
                        obpGwMetricService.save(entity);
                        insertCount++;
                    }
                } catch (DataIntegrityViolationException e) {
                    // Unique constraint violation 발생 시 UPDATE로 재시도
                    log.debug("UK 충돌 발생, UPDATE로 재시도: timeslice={}, apiId={}, clientId={}",
                            timesliceDateTime, vo.getApiId(), vo.getClientId());

                    Optional<ObpGwMetric> existing = obpGwMetricService.findByUniqueKey(
                            timesliceDateTime,
                            vo.getApiId(),
                            vo.getClientId(),
                            vo.getHostname(),
                            vo.getOrgId()
                    );

                    if (existing.isPresent()) {
                        updateExistingMetric(existing.get(), vo, apiNameMap, uriMap, methodMap, appIdMap);
                        updateCount++;
                        conflictRetryCount++;
                    } else {
                        // 재조회에서도 없으면 예외 재발생 (비정상 상황)
                        log.error("UK 충돌 후 재조회 실패: timeslice={}, apiId={}", timesliceDateTime, vo.getApiId());
                        throw e;
                    }
                }
            }

            if (conflictRetryCount > 0) {
                log.info("[4단계] UK 충돌 재시도 건수: {}", conflictRetryCount);
            }

            log.info("[4단계] UPSERT 완료: INSERT={}, UPDATE={}, elapsed={}ms",
                    insertCount, updateCount, System.currentTimeMillis() - step4Start);

            long totalElapsed = System.currentTimeMillis() - jobStartTime;
            log.info("ObpGwMetric 집계 완료: targetHour={}, 총 소요시간={}ms ({}초)",
                    targetHour, totalElapsed, totalElapsed / 1000.0);

        } catch (Exception e) {
            log.error("ObpGwMetric 집계 실패: targetHour={}", targetHour, e);
            throw e;
        }
    }

    /**
     * ObpGwMetricVO 병합 (count 누적)
     */
    private ObpGwMetricVO mergeMetricVO(ObpGwMetricVO existing, ObpGwMetricVO incoming) {
        existing.setAttemptedCount(
                (existing.getAttemptedCount() != null ? existing.getAttemptedCount() : 0L) +
                (incoming.getAttemptedCount() != null ? incoming.getAttemptedCount() : 0L)
        );
        existing.setCompletedCount(
                (existing.getCompletedCount() != null ? existing.getCompletedCount() : 0L) +
                (incoming.getCompletedCount() != null ? incoming.getCompletedCount() : 0L)
        );
        return existing;
    }

    /**
     * 기존 엔티티 업데이트
     */
    private void updateExistingMetric(ObpGwMetric entity, ObpGwMetricVO vo,
                                       Map<String, String> apiNameMap,
                                       Map<String, String> uriMap,
                                       Map<String, String> methodMap,
                                       Map<String, String> appIdMap) {
        entity.setAttemptedCount(vo.getAttemptedCount());
        entity.setCompletedCount(vo.getCompletedCount());
        entity.setApiName(apiNameMap.get(vo.getApiId()));
        entity.setUri(uriMap.get(vo.getEaiSvcSerno()));
        entity.setMethod(methodMap.get(vo.getEaiSvcSerno()));
        entity.setAppId(appIdMap.get(vo.getOrgId()));
        entity.setUpdateCount(entity.getUpdateCount() + 1);
        entity.setUpdatedAt(LocalDateTime.now());
        entity.setUpdatedBy("GwMetricHourJob");
        obpGwMetricService.save(entity);
    }

    /**
     * VO → Entity 변환
     */
    private ObpGwMetric convertToEntity(ObpGwMetricVO vo, Map<String, String> apiNameMap,
                                         Map<String, String> uriMap, Map<String, String> methodMap,
                                         Map<String, String> appIdMap) {
        ObpGwMetric entity = new ObpGwMetric();
        entity.setClientId(vo.getClientId());
        entity.setHostname(vo.getHostname());
        entity.setTimeslice(vo.getTimesliceAsLocalDateTime());
        entity.setApiId(vo.getApiId());
        entity.setOrgId(vo.getOrgId());
        entity.setAttemptedCount(vo.getAttemptedCount());
        entity.setCompletedCount(vo.getCompletedCount());
        entity.setApiName(apiNameMap.get(vo.getApiId()));
        entity.setUri(uriMap.get(vo.getEaiSvcSerno()));       // eaiSvcSerno로 URI 조회
        entity.setMethod(methodMap.get(vo.getEaiSvcSerno())); // eaiSvcSerno로 HTTP Method 조회
        entity.setAppId(appIdMap.get(vo.getOrgId()));
        entity.setUpdateCount(0);
        entity.setCreatedAt(LocalDateTime.now());
        entity.setCreatedBy("GwMetricHourJob");
        return entity;
    }

    /**
     * 특정 시간대 이후의 GwMetric 데이터 조회 (REST API용)
     * after-timeslice 엔드포인트에서 사용
     *
     * <p>조회 범위 제한:</p>
     * <ul>
     *   <li>pageSize 값을 KjbProperty.apiGwMetricPageSize에서 읽어옴 (기본값: 10000)</li>
     *   <li>0일 경우 무제한 조회</li>
     *   <li>정렬: timeslice ASC (오래된 데이터부터 반환)</li>
     * </ul>
     */
    public List<ObpGwMetricDTO> findByTimesliceAfter(LocalDateTime timeslice) {
        DataSourceContextHolder.setDataSourceType(
                DataSourceTypeManager.getDataSourceType(DataSourceTypeManager.MONITORING));

        // KjbProperty에서 pageSize 읽기
        Integer pageSize = null;	//KjbPropertyHolder.get().getApiGwMetricPageSize();
        if (pageSize == null) {
            pageSize = 10000; // 기본값 (null 안전 처리)
        }

        // Service 호출 (limit 적용)
        List<ObpGwMetric> entities = obpGwMetricService.findByTimesliceGreaterThanEqual(timeslice, pageSize);

        return entities.stream()
                .map(ObpGwMetricDTO::from)
                .collect(Collectors.toList());
    }

    /**
     * org.id 목록으로 AppRequest를 조회하여 orgId -> appId 맵 반환
     *
     * <p>AppRequestRepository(@EMSDataSource)를 사용하여 MONITORING 스키마에서 조회</p>
     *
     * @param orgIds 기관 ID 목록
     * @return orgId -> appId 맵
     */
    private Map<String, String> selectAppIdsByOrgIds(Set<String> orgIds) {
        if (orgIds == null || orgIds.isEmpty()) {
            return new HashMap<>();
        }

        List<AppRequest> appRequests = appRequestRepository.findByOrg_IdIn(orgIds);

        Map<String, String> result = new HashMap<>();
        for (AppRequest appRequest : appRequests) {
            String orgId = appRequest.getOrg() != null ? appRequest.getOrg().getId() : null;
            String appId = appRequest.getId();
            if (orgId != null && appId != null) {
                result.put(orgId, appId);
            }
        }
        return result;
    }
}
