package incheon.ags.ias.rst.link.service.impl;

import java.io.IOException;
import java.io.StringReader;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.net.ssl.SSLContext;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContextBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import incheon.ags.ias.rst.link.mapper.AgsRstLinkMapper;
import incheon.ags.ias.rst.link.service.AgsRstLinkService;
import incheon.ags.ias.rst.link.vo.AgsRstKosisVO;
import incheon.ags.ias.rst.link.vo.AgsRstLinkVO;
import lombok.extern.slf4j.Slf4j;

/**
 * @ClassName : AgsRstVoluServiceImpl.java
 * @Description : 부동산통계 API 연계 서비스 구현체
 * @author : 이주훈
 * @since : 2025.10.27
 * @version : 1.0
 */
@Slf4j
@Service
public class AgsRstLinkServiceImpl  implements AgsRstLinkService {
	
    private final AgsRstLinkMapper agsRstLinkMapper;
    
    // 외부 API 호출용 HTTP 클라이언트
    // private final RestTemplate restTemplate = new RestTemplate();
    
    /**
     * SSL 보안이 적용된 RestTemplate
     *  - Bean 초기화 시점에 1회 생성
     */
    private RestTemplate restTemplate; 
    @Autowired private ObjectMapper objectMapper;

    private static final int CONNECT_TIMEOUT = 3000;
    private static final int READ_TIMEOUT = 30000;
    
    @Value("${reb.host}")
    private String rebHost;

    @Value("${kosis.host}")
    private String kosisHost;
    
    public AgsRstLinkServiceImpl(AgsRstLinkMapper agsRstLinkMapper) {
        this.agsRstLinkMapper = agsRstLinkMapper;
    }
    
    /**
     *  Bean 초기화 시 1회 실행
     * - SSLContext 기반 RestTemplate 생성
     */
    @PostConstruct
    public void init() {
        try {
            this.restTemplate = createSecureRestTemplate();
            log.info("RestTemplate initialized (SSL OpenAPI 적용 완료)");
        } catch (Exception e) {
            log.error("RestTemplate 초기화 실패, 기본 RestTemplate 사용", e);
            this.restTemplate = new RestTemplate();
        }
    }
    
    /**
     *  SSL RestTemplate 생성
     */
    private RestTemplate createSecureRestTemplate() throws Exception {

        // 모든 인증서(사설 인증서 / 공공 인증서)를 신뢰하는 SSLContext 생성
        SSLContext sslContext = SSLContextBuilder.create()
                .loadTrustMaterial((chain, authType) -> true)
                .build();
        
        // 허용할 TLS 버전 전부 오픈
        String[] enabledProtocols = new String[]{
                "TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"
        };

        // 특정 IP(host)만 허용
        SSLConnectionSocketFactory socketFactory =
                new SSLConnectionSocketFactory(
                        sslContext,
                        enabledProtocols,
                        null,
                        (hostname, session) -> {
                            boolean isAllowed = hostname.equalsIgnoreCase(rebHost) || hostname.equalsIgnoreCase(kosisHost);
                            if (!isAllowed) {
                                log.error("허용되지 않은 호스트 접근 시도: {}", hostname);
                            }
                            return isAllowed;
                        }
                );

        // Apache HttpClient 생성
        CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(socketFactory)
                .build();

        // RestTemplate에 HttpClient 적용
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);

        // 타임아웃 설정 (LevyNextUtil 기준)
        factory.setConnectTimeout(CONNECT_TIMEOUT);
        factory.setReadTimeout(READ_TIMEOUT);

        return new RestTemplate(factory);
    }
    
    // XXE 방지를 위한 DocumentBuilderFactory 생성 공통 메소드
    private DocumentBuilderFactory getSafeDocumentBuilderFactory() throws ParserConfigurationException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
        factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
        factory.setXIncludeAware(false);
        factory.setExpandEntityReferences(false);
        factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
        factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
        return factory;
    }
    
    /*
     *  한국부동산원 API 연계 서비스 구현
     * 
     * */
    // 한국부동산원 기본 api 인자값 (request)
    private static final String BASE_URL = "https://www.reb.or.kr/r-one/openapi/SttsApiTblData.do"; 		// URL 
    private static final String KEY = "650f2fdab92040b9b4d256b205cce038";                          					// 인증키
    private static final String STATBL_ID = "A_2024_00605";                                       										// 통계표 ID
    private static final String DTACYCLE_CD = "MM";                                               											// 데이터 주기 (월별)
    private static final String ITM_ID = "100001"; 																								// 동(호)수 고정
    private static final int DEFAULT_PAGE_SIZE = 1000; 																					// 최대 요청 건수
    
    // 인천광역시 법정동코드 map 세팅
    private Map<String, AgsRstLinkVO> getRegionMap() {
        Map<String, AgsRstLinkVO> map = new HashMap<>();

        // (API 요청 지역코드, 법정동(API 요청 지역코드, 시도코드, 시군구코드, 읍면동코드))
        map.put("900005", new AgsRstLinkVO("900005", "28", "000", "000")); // 인천광역시
        map.put("910056", new AgsRstLinkVO("910056", "28", "110", "000")); // 중구
        map.put("910057", new AgsRstLinkVO("910057", "28", "140", "000")); // 동구
        map.put("910058", new AgsRstLinkVO("910058", "28", "170", "000")); // 남구(폐기) → (미추홀구로 변경 2018년 7월)
        map.put("910059", new AgsRstLinkVO("910059", "28", "177", "000")); // 미추홀구
        map.put("910060", new AgsRstLinkVO("910060", "28", "185", "000")); // 연수구
        map.put("910061", new AgsRstLinkVO("910061", "28", "200", "000")); // 남동구
        map.put("910062", new AgsRstLinkVO("910062", "28", "237", "000")); // 부평구
        map.put("910063", new AgsRstLinkVO("910063", "28", "245", "000")); // 계양구
        map.put("910064", new AgsRstLinkVO("910064", "28", "260", "000")); // 서구
        map.put("910065", new AgsRstLinkVO("910065", "28", "710", "000")); // 강화군
        map.put("910066", new AgsRstLinkVO("910066", "28", "720", "000")); // 옹진군

        return map;
    }    
    
    // api 호출, 데이터 파싱 후 DB 등록 메소드 (기본 : 1000개)
    @Override
    public Map<String, Object> insertVoluStatsAll(int pageSize) {
    	return fetchAndInsertData(DEFAULT_PAGE_SIZE);
    }
    
    // api 호출, 데이터 파싱 후 DB 등록 메소드 
    public Map<String, Object> fetchAndInsertData(int pageSize) {
        if (pageSize <= 0) pageSize = DEFAULT_PAGE_SIZE;

        Map<String, AgsRstLinkVO> regionMap = getRegionMap();

        // 인천광역시 / 군구 지역코드
        List<String> targetGrpIds = List.of(
            "900005", "910056", "910057", "910058", "910059",
            "910060", "910061", "910062", "910063", "910064",
            "910065", "910066"
        );
        
        // 연계 결과 변수 추가
        int totalInserted = 0;
        int totalErrors = 0;
        
        Map<String, Integer> insertCountMap = new HashMap<>();
        insertCountMap.put("namgu", 0);
        insertCountMap.put("michuhol", 0);
        
        List<Map<String, Object>> results = new ArrayList<>();
        List<String> errorList = new ArrayList<>();

        for (String grpId : targetGrpIds) {
            try {
                AgsRstLinkVO region = regionMap.get(grpId);
                if (region == null) {
                    errorList.add("[" + grpId + "] regionMap에 없음");
                    continue;
                }

                String baseParam = String.format(
                    "%s?STATBL_ID=%s&DTACYCLE_CD=%s&KEY=%s&ITM_ID=%s&GRP_ID=%s",
                    BASE_URL, STATBL_ID, DTACYCLE_CD, KEY, ITM_ID, grpId
                );

                String firstUrl = String.format("%s&pIndex=1&pSize=%d", baseParam, pageSize);
                ResponseEntity<String> firstResponse = restTemplate.getForEntity(firstUrl, String.class);
                if (!firstResponse.getStatusCode().is2xxSuccessful()) {
                    errorList.add("[" + grpId + "] 첫 호출 실패: " + firstResponse.getStatusCode());
                    continue;
                }

                int totalCount = getTotalCount(firstResponse.getBody());
                if (totalCount == 0) totalCount = 1;
                int totalPages = (int) Math.ceil((double) totalCount / pageSize);

                int regionInserted = 0;
                regionInserted += parseAndInsert(firstResponse.getBody(), region, grpId, 1, errorList, insertCountMap);

                for (int page = 2; page <= totalPages; page++) {
                    String pageUrl = String.format("%s&pIndex=%d&pSize=%d", baseParam, page, pageSize);
                    ResponseEntity<String> response = restTemplate.getForEntity(pageUrl, String.class);
                    if (!response.getStatusCode().is2xxSuccessful()) {
                        errorList.add("[" + grpId + "] pIndex=" + page + " 호출 실패");
                        continue;
                    }
                    regionInserted += parseAndInsert(response.getBody(), region, grpId, page, errorList, insertCountMap);
                }

                if ("910058".equals(grpId)) { // 남구(폐지)
                    regionInserted = insertCountMap.get("namgu");
                } else if ("910059".equals(grpId)) { // 미추홀구
                    regionInserted = insertCountMap.get("michuhol");
                }

                totalInserted += regionInserted;

                Map<String, Object> item = new HashMap<>();
                item.put("grpId", grpId);
                item.put("insertedCount", regionInserted);
                item.put("status", "정상");

                if ("910058".equals(grpId)) item.put("status", "폐지");
                else if ("910059".equals(grpId)) item.put("status", "신설");

                results.add(item);

            } catch (RestClientException e) {
            	totalErrors++;
                errorList.add("[" + grpId + "] API 호출 오류");
                log.error("한국부동산원 API 호출 중 RestClientException 발생 (grpId={})", grpId);
			} catch (DataAccessException e) {
				totalErrors++;
                errorList.add("[" + grpId + "] DB 처리 오류");
                log.error("한국부동산원 데이터 저장 중 DataAccessException 발생 (grpId={})", grpId);
			}
        }
        
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("totalInserted", totalInserted);
        result.put("errorCount", totalErrors);
        result.put("results", results);
        result.put("errors", errorList);
        
        result.put("namguInsertCount", insertCountMap.get("namgu"));
        result.put("michuholInsertCount", insertCountMap.get("michuhol"));
        result.put("total_count", insertCountMap.get("namgu") + insertCountMap.get("michuhol"));
        
        return result;
    }
    
    // 총 데이터 건수 리턴 메소드
    private int getTotalCount(String xml) {

        // 입력값 검증
        if (xml == null || xml.trim().isEmpty()) {
            return 0;
        }

        try {
        	// XXE 방지 설정 적용
        	DocumentBuilderFactory factory = getSafeDocumentBuilderFactory(); // 공통 XXE 방지 메소드 호출
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(new InputSource(new StringReader(xml)));

            // 구조 검증
            NodeList totalList = doc.getElementsByTagName("list_total_count");
            if (totalList != null && totalList.getLength() > 0) {
                String value = totalList.item(0).getTextContent();

                if (value != null && !value.trim().isEmpty()) {
                    int count = Integer.parseInt(value.trim());

                    // 음수 불가
                    return Math.max(count, 0);
                }
            }

        } catch (NumberFormatException e) {
            // 숫자 변환 실패
        	log.warn("list_total_count 값 형식 오류");
        } catch (ParserConfigurationException e) {
            log.error("XML 처리 중 ParserConfigurationException 발생");
        } catch (SAXException e) {
            log.error("XML 처리 중 SAXException 발생");
        } catch (IOException e) {
            log.error("XML 처리 중 IOException 발생");
        }

        return 0;
    }

    // response(xml) 파싱 후 db 등록
    private int parseAndInsert(String xml, AgsRstLinkVO agsRstLinkVO, String grpId, int pageIndex, List<String> errorList, Map<String, Integer> insertCountMap) {
    	int insertedCount = 0;
        try {
        	// XXE 방지 설정 적용
        	DocumentBuilderFactory factory = getSafeDocumentBuilderFactory();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document doc = builder.parse(new InputSource(new StringReader(xml)));

            NodeList rows = doc.getElementsByTagName("row");
            if (rows == null || rows.getLength() == 0) {
            	errorList.add(String.format("[%s] pIndex=%d 데이터 없음", grpId, pageIndex));
                return 0;
            }

            for (int i = 0; i < rows.getLength(); i++) {
                Element row = (Element) rows.item(i);

                String wrttime = getValue(row, "WRTTIME_IDTFR_ID");
                String clsId = getValue(row, "CLS_ID");
                String hsTypeCd = mapHsTypeCode(clsId);
                String itmId = getValue(row, "ITM_ID");
                String dtaVal = getValue(row, "DTA_VAL");

                if (wrttime.isEmpty() || itmId.isEmpty()) continue;

                AgsRstLinkVO vo = new AgsRstLinkVO();
                vo.setCrtrYm(wrttime);
                vo.setSttyCtpvCd(agsRstLinkVO.getSttyCtpvCd());
                vo.setSttyEmdCd(agsRstLinkVO.getSttyEmdCd());
                vo.setCtrtMthdCd("1"); // 매매
                vo.setHsTypeSeCd(hsTypeCd);

                try {
                    if ("100001".equals(itmId)) { // 동(호)수
                        vo.setDlgnNocs(Integer.parseInt(dtaVal));
                    }
                } catch (NumberFormatException e) {
                	errorList.add(String.format("[%s] 숫자 변환 실패 (%s, %s): %s", grpId, wrttime, itmId, dtaVal));
                    continue;
                }

                try {
                    // 남구 폐기 데이터 저장
                    if ("910058".equals(grpId)) {
                        vo.setSttySggCd("170");
                        try {
                            agsRstLinkMapper.insertRstVoluStats(vo);
                            insertCountMap.put("namgu", insertCountMap.get("namgu") + 1);
                        } catch (DuplicateKeyException e) {
                            // 중복 키 예외
                        	log.info("[남구] 중복 데이터 제외: {}, {}", grpId, wrttime);
                        } catch (DataAccessException e) {
                            errorList.add(String.format("[%s] insert 실패 (%s, 남구)", grpId, wrttime));
                        }

                        // 남구 폐기 데이터 미추홀구에 저장
                        if (Integer.parseInt(wrttime) < 201807) {
                            AgsRstLinkVO copy = new AgsRstLinkVO();
                            copy.setCrtrYm(vo.getCrtrYm());
                            copy.setSttyCtpvCd(vo.getSttyCtpvCd());
                            copy.setSttySggCd("177");
                            copy.setSttyEmdCd(vo.getSttyEmdCd());
                            copy.setCtrtMthdCd(vo.getCtrtMthdCd());
                            copy.setHsTypeSeCd(hsTypeCd);
                            copy.setDlgnNocs(vo.getDlgnNocs());
                            try {
                                agsRstLinkMapper.insertRstVoluStats(copy);
                                insertCountMap.put("michuhol", insertCountMap.get("michuhol") + 1);
                            } catch (DuplicateKeyException e) {
                            	// 중복 키 예외
                            	log.info("[남구 복사 데이터 미추홀로] 중복 데이터 제외: {}, {}", grpId, wrttime);
                            } catch (DataAccessException e) {
                                errorList.add(String.format("[%s] insert 실패 (%s, 미추홀 복사)", grpId, wrttime));
                            } 
                        }
                    }
                    // 미추홀구 데이터 저장
                    else if ("910059".equals(grpId)) {
                        vo.setSttySggCd("177");
                        try {
                            agsRstLinkMapper.insertRstVoluStats(vo);
                            insertCountMap.put("michuhol", insertCountMap.get("michuhol") + 1);
                        } catch (DuplicateKeyException e) {
                            // 중복 키 예외
                        	log.info("[미추홀] 중복 데이터 제외: {}, {}", grpId, wrttime);
                        } catch (DataAccessException e) {
                            errorList.add(String.format("[%s] insert 실패 (%s, 미추홀)", grpId, wrttime));
                        }
                    }
                    // 이외 구 저장
                    else {
                        vo.setSttySggCd(agsRstLinkVO.getSttySggCd());
                        try {
                            agsRstLinkMapper.insertRstVoluStats(vo);
                            insertedCount++;
                        } catch (DuplicateKeyException e) {
                            // 중복 키 예외
                        	log.info("[기타구] 중복 데이터 제외: {}, {}", grpId, wrttime);
                        } catch (DataAccessException e) {
                            errorList.add(String.format("[%s] insert 실패 (%s)", grpId, wrttime));
                        }
                    }

                } catch (DataAccessException e) {
                    errorList.add(String.format("[%s] 데이터 저장중 DB 오류 발생 (%s)", grpId, wrttime));
                }
            }

        } catch (ParserConfigurationException e) {
            errorList.add(String.format("[%s] pIndex=%d ParserConfigurationException 발생", grpId, pageIndex));
            log.error("XML 파서 설정 오류");
        } catch (SAXException e) {
            errorList.add(String.format("[%s] pIndex=%d SAXException 발생", grpId, pageIndex));
            log.error("XML 파싱 오류");
        } catch (IOException e) {
            errorList.add(String.format("[%s] pIndex=%d IOException 발생", grpId, pageIndex));
            log.error("XML 입출력 오류");
        }

        return insertedCount;
    }

    // tagName값 리턴 메소드
    private String getValue(Element elem, String tagName) {
        NodeList list = elem.getElementsByTagName(tagName);
        return list.getLength() > 0 ? list.item(0).getTextContent().trim() : "";
    }
    
    // 주택유형 코드 
    private String mapHsTypeCode(String clsId) {
        switch (clsId) {
            case "500001": return "0"; // 합계
            case "500002": return "4"; // 단독주택
            case "500003": return "5"; // 다가구주택
            case "500004": return "3"; // 다세대주택
            case "500005": return "2"; // 연립주택
            case "500006": return "1"; // 아파트
            default: return clsId; 
        }
    }
    
    @Override
    public Map<String, Object> insertVoluStatsIncremental(int pageSize) {
        if (pageSize <= 0) pageSize = DEFAULT_PAGE_SIZE;

        // DB에서 최신 기준년월 조회
        String latestDbYm = agsRstLinkMapper.selectMaxVoluCrtrYm(); 	// 202509
        if (latestDbYm == null) latestDbYm = "200601"; 								// 최소 시작 기준월
        latestDbYm = latestDbYm.trim();
        
        // API에서 최신 기준년월 확인
        String latestApiYm = getLatestOneYm(latestDbYm);
        if (latestApiYm == null)
        	latestApiYm = YearMonth.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
        latestApiYm = latestApiYm.trim();
        
        System.out.printf("[DEBUG] DB=%s, API=%s, compare=%d%n",
                latestDbYm, latestApiYm, latestDbYm.compareTo(latestApiYm));
        
        // 최신 데이터가 이미 반영된 경우
        if (latestDbYm.compareTo(latestApiYm) >= 0) {
            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("message", "※ 최신 데이터까지 이미 반영된 상태입니다.");
            result.put("totalInserted", 0);
            return result;
        }

        // DB의 최신 이후부터 호출 및 DB 적재
        return fetchAndInsertIncrementalData(latestDbYm, latestApiYm, pageSize);
    }
    
    private String getLatestOneYm(String latestDbYm) {
        try {
            String startYear = latestDbYm.substring(0, 4);

            String url = String.format(
                "%s?STATBL_ID=%s&DTACYCLE_CD=%s&KEY=%s&ITM_ID=%s&GRP_ID=900005&START_WRTTIME=%s&pIndex=1&pSize=1000",
                BASE_URL, STATBL_ID, DTACYCLE_CD, KEY, ITM_ID, startYear
            );

            String xml = restTemplate.getForObject(url, String.class);
            
            // XXE 방지 설정 적용
            DocumentBuilderFactory factory = getSafeDocumentBuilderFactory();
            Document doc = factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
            NodeList rows = doc.getElementsByTagName("row");
            
            if (rows.getLength() == 0) {
                System.out.println("[DEBUG] API 응답에 row가 없습니다 (startYear=" + startYear + ")");
                return YearMonth.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
            }

            int latestApiYm = Integer.parseInt(latestDbYm);

            for (int i = 0; i < rows.getLength(); i++) {
                Element row = (Element) rows.item(i);
                String wrttime = getValue(row, "WRTTIME_IDTFR_ID").trim();

                if (wrttime.matches("\\d{6}")) {
                    int ym = Integer.parseInt(wrttime);
                    if (ym > latestApiYm)
                    	latestApiYm = ym;
                }
            }

            System.out.println("[DEBUG] getLatestOneYm(): 최신 API 기준월 = " + latestApiYm + " (startYear=" + startYear + ")");
            return String.valueOf(latestApiYm);

        } catch (ParserConfigurationException e) {
            log.error("최신 기준월 조회 실패 - ParserConfigurationException");
        } catch (SAXException e) {
            log.error("최신 기준월 조회 실패 - SAXException");
        } catch (IOException e) {
            log.error("최신 기준월 조회 실패 - IOException");
        } catch (RestClientException e) {
            log.error("최신 기준월 조회 실패 - RestClientException");
        }

        return YearMonth.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
    }
    
    private Map<String, Object> fetchAndInsertIncrementalData(String startYm, String endYm, int pageSize) {
        Map<String, Object> result = new HashMap<>();
        List<String> errorList = new ArrayList<>();
        Map<String, AgsRstLinkVO> regionMap = getRegionMap();
        List<String> targetGrpIds = List.of(
            "900005", "910056", "910057", "910058", "910059",
            "910060", "910061", "910062", "910063", "910064",
            "910065", "910066"
        );

        int totalInserted = 0;
        int totalErrors = 0;
        List<Map<String, Object>> resultsList = new ArrayList<>();
        
        Map<String, Integer> insertCountMap = new HashMap<>();
        insertCountMap.put("namgu", 0);
        insertCountMap.put("michuhol", 0);

        YearMonth start = YearMonth.parse(startYm, DateTimeFormatter.ofPattern("yyyyMM")).plusMonths(1);
        YearMonth end = YearMonth.parse(endYm, DateTimeFormatter.ofPattern("yyyyMM"));
        String actualStartYm = start.format(DateTimeFormatter.ofPattern("yyyyMM"));

        List<String> ymList = new ArrayList<>();
        for (YearMonth ym = start; !ym.isAfter(end); ym = ym.plusMonths(1)) {
            ymList.add(ym.format(DateTimeFormatter.ofPattern("yyyyMM")));
        }

        for (String grpId : targetGrpIds) {
            try {
                AgsRstLinkVO region = regionMap.get(grpId);
                if (region == null) {
                    errorList.add("[" + grpId + "] regionMap에 없음");
                    continue;
                }

                int regionInsertedTotal = 0;

                for (String ym : ymList) {
                    String url = String.format(
                        "%s?STATBL_ID=%s&DTACYCLE_CD=%s&KEY=%s&ITM_ID=%s&GRP_ID=%s&WRTTIME_IDTFR_ID=%s&pIndex=1&pSize=%d",
                        BASE_URL, STATBL_ID, DTACYCLE_CD, KEY, ITM_ID, grpId, ym, pageSize
                    );

                    ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
                    if (!response.getStatusCode().is2xxSuccessful()) {
                        errorList.add("[" + grpId + "] " + ym + " 호출 실패: " + response.getStatusCode());
                        continue;
                    }

                    int inserted = parseAndInsert(response.getBody(), region, grpId, 1, errorList, insertCountMap);
                    regionInsertedTotal += inserted;
                    totalInserted += inserted;
                }
                
                if ("910058".equals(grpId)) {
                    regionInsertedTotal = insertCountMap.get("namgu");
                } else if ("910059".equals(grpId)) {
                    regionInsertedTotal = insertCountMap.get("michuhol");
                }

                Map<String, Object> regionResult = new HashMap<>();
                regionResult.put("grpId", grpId);
                regionResult.put("insertedCount", regionInsertedTotal);
                resultsList.add(regionResult);

            } catch (RestClientException e) {
                totalErrors++;
                errorList.add("[" + grpId + "] 증분 처리 중 API 호출 오류");
                log.error("증분 처리 API 호출 중 RestClientException 발생 (grpId={})", grpId);
            } catch (DataAccessException e) {
                totalErrors++;
                errorList.add("[" + grpId + "] 증분 처리 중 DB 오류");
                log.error("증분 처리 중 DataAccessException 발생 (grpId={})", grpId);
            }
        }
        
        totalInserted += insertCountMap.get("namgu") + insertCountMap.get("michuhol");
        result.put("success", true);
        result.put("totalInserted", totalInserted);
        result.put("errorCount", totalErrors);
        result.put("errors", errorList);
        result.put("results", resultsList);

        String messageRange = actualStartYm.equals(endYm)
        	    ? String.format("※ 증분 연계 완료 (%s)", endYm)
        	    : String.format("※ 증분 연계 완료 (%s ~ %s)", actualStartYm, endYm);

    	result.put("message", messageRange);

        return result;
    }
    
    
    /*
     *  국가데이터처(KOSIS) API 연계 서비스 구현
     * 
     * */
    // 국가데이터처(KOSIS) 기본 api 인자값 (request)
    private static final String KOSIS_BASE_URL = "https://kosis.kr/openapi/Param/statisticsParameterData.do?method=getList";
    private static final String KOSIS_API_KEY = "NWVjY2Q0NmMxMDBmZjQ0MWUzNjVhYWJjNTgyMDZmMzk=";
    private static final String KOSIS_ORG_ID = "101";
    private static final String KOSIS_TBL_ID = "DT_1B26003_A01";
    private static final String KOSIS_ITM_ID = "T70+T80+";
    private static final String KOSIS_PRD_SE = "M";
    private static final String KOSIS_OUTPUT_FIELDS = "OBJ_ID+NM+ITM_ID+ITM_NM+PRD_SE+PRD_DE+LST_CHN_DE+";
    private static final String[] KOSIS_DIRECTIONS = {
        "&objL1=28&objL2=ALL", // 인천(전출지) → 시도(전입지)
        "&objL1=ALL&objL2=28"  // 시도(전출지) → 인천 (전입지)
    };
    
    @Override
    public Map<String, Object> insertPpltnStatsAll() {
        return fetchAndInsertPpltnStats("198101", getLatestKosisYm());
    }
    
    @Override
    public Map<String, Object> insertPpltnStatsIncremental() {
        String latestDbYm = agsRstLinkMapper.selectMaxKosisCrtrYm();
        if (latestDbYm == null) latestDbYm = "198101";
        String latestKosisYm = getLatestKosisYm();

        YearMonth start = YearMonth.parse(latestDbYm, DateTimeFormatter.ofPattern("yyyyMM")).plusMonths(1);
        YearMonth end = YearMonth.parse(latestKosisYm, DateTimeFormatter.ofPattern("yyyyMM"));

        if (start.isAfter(end)) {
            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("message", "※ 최신 데이터까지 이미 반영된 상태입니다.");
            result.put("totalInserted", 0);
            return result;
        }

        return fetchAndInsertPpltnStats(start.format(DateTimeFormatter.ofPattern("yyyyMM")), latestKosisYm);
    }
    
    private Map<String, Object> fetchAndInsertPpltnStats(String startYm, String endYm) {
        int totalInserted = 0, totalErrors = 0, inCount = 0, outCount = 0;
        List<String> errorList = new ArrayList<>();

        // 1. 처리할 연월 리스트 생성
        List<String> ymList = new ArrayList<>();
        YearMonth start = YearMonth.parse(startYm, DateTimeFormatter.ofPattern("yyyyMM"));
        YearMonth end = YearMonth.parse(endYm, DateTimeFormatter.ofPattern("yyyyMM"));
        for (YearMonth ym = start; !ym.isAfter(end); ym = ym.plusMonths(1)) {
            ymList.add(ym.format(DateTimeFormatter.ofPattern("yyyyMM")));
        }

        // 2. 월 단위로 루프 실행
        for (String ym : ymList) {
            // T70/T80 데이터를 병합하기 위한 Map (매월 초기화)
            Map<String, AgsRstKosisVO> monthlyMergeMap = new LinkedHashMap<>();

            for (String dir : KOSIS_DIRECTIONS) {
                String url = new StringBuilder(KOSIS_BASE_URL)
                        .append("&apiKey=").append(KOSIS_API_KEY)
                        .append("&orgId=").append(KOSIS_ORG_ID)
                        .append("&tblId=").append(KOSIS_TBL_ID)
                        .append("&itmId=").append(KOSIS_ITM_ID)
                        .append("&format=json")
                        .append("&jsonVD=Y")
                        .append("&prdSe=").append(KOSIS_PRD_SE)
                        .append("&startPrdDe=").append(ym) 	// 해당 월만 호출
                        .append("&endPrdDe=").append(ym)   	// 해당 월만 호출
                        .append("&outputFields=").append(KOSIS_OUTPUT_FIELDS)
                        .append(dir)
                        .toString();

                try {
                    String raw = restTemplate.getForObject(url, String.class);
                    if (raw == null || !raw.trim().startsWith("[")) continue;

                    List<Map<String, Object>> dataList = objectMapper.readValue(raw, new TypeReference<List<Map<String, Object>>>() {});
                    
                    int dirProcessedCount = 0;
                    
                    for (Map<String, Object> item : dataList) {
                        try {
                            String crtrYm = (String) item.get("PRD_DE");
                            String mvout = (String) item.get("C1");
                            String mvin = (String) item.get("C2");
                            String itmId = (String) item.get("ITM_ID");
                            
                            if ("28".equals(mvout) && "28".equals(mvin)) continue;
                            
                            // T70(이동자수)일 때만 카운트 (T70/T80 한 쌍이므로)
                            if ("T70".equals(itmId)) {
                                dirProcessedCount++;
                            }

                            String key = String.join("_", crtrYm, mvout, mvin);
                            AgsRstKosisVO vo = monthlyMergeMap.getOrDefault(key, new AgsRstKosisVO());
                            vo.setCrtrYm(crtrYm);
                            vo.setSttyCtpvCd("28");
                            vo.setMvoutCtpvCd(mvout);
                            vo.setMvinCtpvCd(mvin);

                            int value = (int) Double.parseDouble(String.valueOf(item.get("DT")));

                            if ("T70".equals(itmId)) vo.setPpltnCnt(value);
                            else if ("T80".equals(itmId)) vo.setIcdcPpltnCnt(value);

                            monthlyMergeMap.put(key, vo);
                        } catch (NumberFormatException e) {
                            totalErrors++;
                            errorList.add(ym + " 데이터 파싱 실패 (숫자 변환 오류)");
                            log.error("KOSIS 데이터 파싱 중 NumberFormatException 발생");
                        } catch (NullPointerException e) {
                            totalErrors++;
                            errorList.add(ym + " 데이터 파싱 실패 (데이터 누락)");
                            log.error("KOSIS 데이터 파싱 중 NullPointerException 발생");
                        }
                    }
                    
                    if (dir.contains("objL1=28")) {
                        outCount += dirProcessedCount; // 인천 -> 타지역
                    } else {
                        inCount += dirProcessedCount;  // 타지역 -> 인천
                    }

                } catch (JsonProcessingException e) {
                    totalErrors++;
                    errorList.add(ym + " KOSIS 데이터 JSON 처리 오류");
                    log.error("KOSIS JSON 처리 중 예외 발생 (ym={})", ym);
                } catch (RestClientException e) {
                    totalErrors++;
                    errorList.add(ym + " KOSIS API 통신 오류");
                    log.error("KOSIS API 통신 중 예외 발생 (ym={})", ym);
                }
            }

            // 3. 월별 데이터 DB Insert
            for (AgsRstKosisVO vo : monthlyMergeMap.values()) {
                try {
                    int result = agsRstLinkMapper.insertPpltnStats(vo);
                    if (result > 0) totalInserted++;
                } catch (DataAccessException e) {
                    totalErrors++;
                    errorList.add(ym + " DB 저장 오류");
                    log.error("KOSIS 데이터 저장 중 DataAccessException 발생 (ym={})", ym);
                }
            }
            
            // 모든 데이터 적재 후 공간정보 갱신
            try {
                agsRstLinkMapper.updateGeomByYm(ym);
            } catch (DataAccessException e) {
                log.error("공간정보 업데이트 중 DB 오류 발생 (ym={})", ym);
            }
        }

        // 결과 맵 구성
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("outCount", outCount);
        result.put("inCount", inCount);
        result.put("totalInserted", totalInserted);
        result.put("errorCount", totalErrors);
        result.put("errors", errorList);
        
        String messageRange = startYm.equals(endYm)
                ? String.format("※ 연계 완료 (%s)", endYm)
                : String.format("※ 연계 완료 (%s ~ %s)", startYm, endYm);
        result.put("message", messageRange);
        
        System.out.printf("KOSIS 적재 완료: 총 %d건 (전출 %d / 전입 %d / 오류 %d)%n",
                totalInserted, outCount, inCount, totalErrors);

        return result;
    }
    
    private String getLatestKosisYm() {
        try {
            String url = KOSIS_BASE_URL
                    + "&apiKey=" + KOSIS_API_KEY
                    + "&orgId=" + KOSIS_ORG_ID
                    + "&tblId=" + KOSIS_TBL_ID
                    + "&itmId=" + KOSIS_ITM_ID
                    + "&format=json"
                    + "&jsonVD=Y"
                    + "&prdSe=" + KOSIS_PRD_SE
                    + "&newEstPrdCnt=1"
                    + "&outputFields=" + KOSIS_OUTPUT_FIELDS
                    + "&objL1=28&objL2=ALL";

            String raw = restTemplate.getForObject(url, String.class);
            if (raw != null && raw.trim().startsWith("[")) {
                List<Map<String, Object>> list = objectMapper.readValue(raw, new TypeReference<List<Map<String, Object>>>() {});
                if (!list.isEmpty()) {
                    return (String) list.get(0).get("PRD_DE");
                }
            }
        } catch (JsonProcessingException e) {
        	log.warn("KOSIS 최신 기준연월 조회 실패 - JsonProcessingException");
        } catch (RestClientException e) {
            log.warn("KOSIS 최신 기준연월 조회 실패 - RestClientException");
        }
        return YearMonth.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
    }
    
    @Override
    public int insertPrcStatsAll() {
    	try {
            return agsRstLinkMapper.insertPrcStatsAll();
        } catch (DataAccessException e) {
            log.error("insertPrcStatsAll 처리 중 DB 오류 발생");
            return 0;
        }
    }

}