package incheon.ags.dss.decline.service.impl;

import incheon.ags.dss.decline.mapper.AnaZoneDgnsDtlMapper;
import incheon.ags.dss.decline.service.AnaZoneDgnsDtlService;
import incheon.ags.dss.decline.service.PredictServce;
import incheon.ags.dss.decline.vo.AnaPredictScaVO;
import incheon.ags.dss.decline.vo.AnaZoneDgnsDtlVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service("predictServceImpl")
@RequiredArgsConstructor
@Slf4j
public class PredictServceImpl implements PredictServce {

    private final AnaZoneDgnsDtlMapper mapper;

    @Override
    @Async("dssAnalysisExecutor")
//    @Transactional
    public void executeJavaPrediction(String dgnsNo, int zoneNo, int baseYear, int targetYear, String userId) {
        log.info(">>> [Async Start] Diagnosis No: {}", dgnsNo);
        
        // 1. [시작] 상태를 'RUNNING'으로 변경
        updateStatus(dgnsNo, "RUNNING");
        
        try {
        	
//        	// ▼▼▼ [테스트용] 강제로 에러 발생 ▼▼▼
//            if (true) {
//                Thread.sleep(20000); // 로딩바가 도는 걸 20초 정도 구경하기 위해 딜레이
//                throw new RuntimeException(">>> [TEST] 의도적인 강제 에러 발생 테스트입니다! <<<");
//            }
//            // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
        	
            mapper.deleteAnaPredictSca(dgnsNo);
            
            List<String> scpCodes = mapper.selectIntersectingScpCodes(zoneNo);
            if (scpCodes == null || scpCodes.isEmpty()) {
            	log.warn("분석 대상 집계구가 없습니다. No: {}", dgnsNo);
            	// 데이터 없음 처리 (NO_DATA 혹은 SUCCESS)
            	updateStatus(dgnsNo, "NO_DATA");
            	return;
            }

            List<AnaPredictScaVO> batchList = new ArrayList<>();
            for (String scpCd : scpCodes) {
                Map<String, Object> params = new HashMap<>();
                params.put("scpCd", scpCd);
                params.put("baseYear", baseYear);

                List<Map<String, Object>> histData = mapper.selectHistoricalStatsForPredict(params);
                List<AnaPredictScaVO> results = calculatePrediction(dgnsNo, scpCd, histData, baseYear, targetYear, userId);

                batchList.addAll(results);
                if (batchList.size() >= 1000) {
                    mapper.insertAnaPredictScaBatch(batchList);
                    batchList.clear();
                }
            }
            if (!batchList.isEmpty()) {
                mapper.insertAnaPredictScaBatch(batchList);
            }
            
            // 2. [완료] 상태를 'SUCCESS'로 변경
            updateStatus(dgnsNo, "SUCCESS");
            log.info(">>> [Async End] Diagnosis Success: {}", dgnsNo);
            
        } catch (Exception e) {
//            log.error("Diagnosis Failed", e);
        	// 1. 보안 조치: 에러 로그에 시스템 내부 정보(e)는 남기되, 메시지는 관리함
            log.error("예측 데이터 진단 배치 처리 중 치명적 오류 발생", e);
            
            // 에러 발생 시 last_mdfcn_id를 'FAIL'로 변경
            updateStatus(dgnsNo, "FAIL");
            
            // 2. 로직 조치: 배치를 '실패' 상태로 만들기 위해 예외를 다시 던짐 (트랜잭션 롤백)
//            throw new RuntimeException("Diagnosis Batch Failed", e);
        }
    }
    
    // 상태 업데이트 헬퍼
    private void updateStatus(String dgnsNo, String status) {
        try {
            AnaZoneDgnsDtlVO vo = new AnaZoneDgnsDtlVO();
            vo.setDgnsNo(dgnsNo);
            vo.setLastMdfcnId(status); // 수정자ID 컬럼 재활용
            mapper.updateDiagnosisStatus(vo);
        } catch (Exception ex) {
            log.error("상태 업데이트 실패", ex);
        }
    }

    private List<AnaPredictScaVO> calculatePrediction(String dgnsNo, String scpCd, List<Map<String, Object>> histData, int baseYear, int targetYear, String userId) {

        Map<Integer, Double> popMap = new HashMap<>();
        Map<Integer, Double> bizMap = new HashMap<>();
        Map<Integer, Double> totalBldgMap = new HashMap<>();
        Map<Integer, Double> oldBldgMap = new HashMap<>();

        int minYear = 9999;

        // 1. 데이터 정리
        for (Map<String, Object> row : histData) {
            Object bayeObj = row.get("baye");
            Object valObj = row.get("val");
            Object typeObj = row.get("stats_artcl");
            Object constYearObj = row.get("const_year");

            if (bayeObj == null || valObj == null || typeObj == null) continue;

            try {
                int year = Integer.parseInt(String.valueOf(bayeObj));
                double val = Double.parseDouble(String.valueOf(valObj));
                // [수정 핵심 1] 공백 제거 (.trim()) 추가
                String type = String.valueOf(typeObj).trim();

                if (year < minYear) minYear = year;

                if ("to_in_001".equals(type)) { // 총인구
                    popMap.put(year, val);
                } else if ("to_fa_010".equals(type)) { // 총사업체
                    bizMap.put(year, val);
                } else if (type.startsWith("ho_yr_")) {
                    totalBldgMap.merge(year, val, Double::sum);
                    if (constYearObj != null) {
                        int constYear = Integer.parseInt(String.valueOf(constYearObj));
                        if ((year - constYear) >= 20) {
                            oldBldgMap.merge(year, val, Double::sum);
                        }
                    }
                }
            } catch (NumberFormatException e) {
                continue;
            }
        }

        if (minYear == 9999) minYear = baseYear;

        // 2. 선형 회귀분석 파라미터 산출
        double[] popReg = calculateLinearRegression(popMap);
        double[] bizReg = calculateLinearRegression(bizMap);

        Map<Integer, Double> oldRatioMap = new HashMap<>();
        for (Integer year : totalBldgMap.keySet()) {
            double total = totalBldgMap.get(year);
            double old = oldBldgMap.getOrDefault(year, 0.0);
            if (total > 0) {
                oldRatioMap.put(year, old / total);
            }
        }
        double[] oldRatioReg = calculateLinearRegression(oldRatioMap);

        List<AnaPredictScaVO> resultList = new ArrayList<>();

        // 3. 연도 루프
        for (int yr = minYear; yr <= targetYear; yr++) {
            AnaPredictScaVO vo = new AnaPredictScaVO();
            vo.setDgnsNo(dgnsNo);
            vo.setScaScpCd(scpCd);
            vo.setBaye(yr);
            vo.setUserId(userId);
            vo.setPredcYn(yr > baseYear ? "Y" : "N");

            double popVal = popMap.containsKey(yr) ? popMap.get(yr) : predictValue(popReg, yr);
            double bizVal = bizMap.containsKey(yr) ? bizMap.get(yr) : predictValue(bizReg, yr);

            double oldRatioVal = oldRatioMap.containsKey(yr) ? oldRatioMap.get(yr) : predictValue(oldRatioReg, yr);
            oldRatioVal = Math.max(0.0, Math.min(1.0, oldRatioVal));

            vo.setTppl(BigDecimal.valueOf(popVal));
            vo.setTotBzent(BigDecimal.valueOf(bizVal));
            vo.setSpanuatBdstRt(BigDecimal.valueOf(oldRatioVal));

            resultList.add(vo);
        }

        // 4. 후처리: 파생 지표 계산
        calculateDerivedMetrics(resultList);

        return resultList;
    }

    // [수정 핵심 2] 데이터가 1개 이하일 때 0으로 떨어지는 문제 해결
    private double[] calculateLinearRegression(Map<Integer, Double> data) {
        if (data == null || data.isEmpty()) return new double[]{0, 0};

        double n = data.size();

        // 데이터가 1개뿐이면 기울기(slope)는 0, 절편(intercept)은 그 값 자체로 설정
        if (n == 1) {
            double y = data.values().iterator().next();
            return new double[]{0, y};
        }

        double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
        for (Map.Entry<Integer, Double> entry : data.entrySet()) {
            double x = entry.getKey();
            double y = entry.getValue();
            sumX += x; sumY += y; sumXY += x * y; sumX2 += x * x;
        }

        double denom = (n * sumX2 - sumX * sumX);

        // 분모가 0인 경우(모든 X가 같은 경우) 처리
        if (denom == 0) {
            double avgY = sumY / n;
            return new double[]{0, avgY};
        }

        double slope = (n * sumXY - sumX * sumY) / denom;
        double intercept = (sumY - slope * sumX) / n;

        return new double[]{slope, intercept};
    }

    private double predictValue(double[] regression, int x) {
        double val = regression[0] * x + regression[1];
        return Math.max(0, val);
    }

    private void calculateDerivedMetrics(List<AnaPredictScaVO> list) {
        for (int i = 0; i < list.size(); i++) {
            AnaPredictScaVO curr = list.get(i);

            double currPop = curr.getTppl() != null ? curr.getTppl().doubleValue() : 0.0;
            double currBiz = curr.getTotBzent() != null ? curr.getTotBzent().doubleValue() : 0.0;

            // 1. 전년 대비 증감율 (YOY)
            if (i > 0) {
                AnaPredictScaVO prev = list.get(i-1);
                double prevPop = prev.getTppl().doubleValue();
                double prevBiz = prev.getTotBzent().doubleValue();

                curr.setPpltnCntYoe(prevPop > 0 ? BigDecimal.valueOf((currPop - prevPop) / prevPop) : BigDecimal.ZERO);
                curr.setBzentCntYoe(prevBiz > 0 ? BigDecimal.valueOf((currBiz - prevBiz) / prevBiz) : BigDecimal.ZERO);
            } else {
                curr.setPpltnCntYoe(BigDecimal.ZERO);
                curr.setBzentCntYoe(BigDecimal.ZERO);
            }

            // 2. 30년 내 최고 대비 비율 (인구)
            double maxPop30 = 0.0;
            for (int k = 0; k <= 30; k++) {
                if (i - k < 0) break;
                double val = list.get(i - k).getTppl().doubleValue();
                if (val > maxPop30) maxPop30 = val;
            }
            curr.setPpltnCnt30yePrd(maxPop30 > 0 ? BigDecimal.valueOf(currPop / maxPop30) : BigDecimal.ONE); // 기본값 1.0 (변동 없음)

            // 3. 10년 내 최고 대비 비율 (사업체)
            double maxBiz10 = 0.0;
            for (int k = 0; k <= 10; k++) {
                if (i - k < 0) break;
                double val = list.get(i - k).getTotBzent().doubleValue();
                if (val > maxBiz10) maxBiz10 = val;
            }
            curr.setBzentCnt10yePrd(maxBiz10 > 0 ? BigDecimal.valueOf(currBiz / maxBiz10) : BigDecimal.ONE);

            // 4. 5년 연속 감소 (Population)
            int popDeclineCount = 0;
            for (int k = 1; k <= 5; k++) {
                if (i - k < 0) break;
                if (list.get(i - k + 1).getTppl().compareTo(list.get(i - k).getTppl()) < 0) {
                    popDeclineCount++;
                } else {
                    break;
                }
            }
            curr.setPpltnCnt5yeaPrd(popDeclineCount);

            // 5. 5년 연속 감소 (Business)
            int bizDeclineCount = 0;
            for (int k = 1; k <= 5; k++) {
                if (i - k < 0) break;
                if (list.get(i - k + 1).getTotBzent().compareTo(list.get(i - k).getTotBzent()) < 0) {
                    bizDeclineCount++;
                } else {
                    break;
                }
            }
            curr.setBzentCnt5yeaPrd(bizDeclineCount);
        }
    }

}
