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

import java.math.BigDecimal;
import java.net.URI;
import java.time.LocalDate; // [ADD] 추가
import java.time.format.DateTimeFormatter; // [ADD] 추가
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

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

import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; // [ADD] 추가

import incheon.ags.dss.common.mapper.DssCommonMapper;
import incheon.ags.dss.under.mapper.UrbUdgdSnkgHistMapper;
import incheon.ags.dss.under.service.UrbUdgdSnkgHistService;
import incheon.ags.dss.under.vo.UrbUdgdSnkgHistVO;
import incheon.ags.dss.under.web.dto.UrbUdgdSnkgOpenApiDetailResDTO;
import incheon.ags.dss.under.web.dto.UrbUdgdSnkgOpenApiListResDTO;
import incheon.ags.dss.under.web.dto.UrbUdgdSnkgOpenApiReqDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service("urbUdgdSnkgHistService")
@RequiredArgsConstructor
@Slf4j
public class UrbUdgdSnkgHistServiceImpl implements UrbUdgdSnkgHistService {

    // [DI] RestTemplate, ObjectMapper 주입 (Config에 Bean 등록되어 있어야 함)
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;
    private final DssCommonMapper dssCommonMapper; // [ADD] Common Mapper 주입

    @Value("${dss.openapi.sinkhole.url-list}")
    private String baseUrlList;

    @Value("${dss.openapi.sinkhole.url-detail}")
    private String baseUrlDetail;

    @Value("${dss.openapi.sinkhole.service-key}")
    private String serviceKey;
    
    private final UrbUdgdSnkgHistMapper urbUdgdSnkgHistMapper;

    @Override
    public UrbUdgdSnkgHistVO selectUrbUdgdSnkgHistDetail(UrbUdgdSnkgHistVO vo) throws Exception {
        return urbUdgdSnkgHistMapper.selectUrbUdgdSnkgHistDetail(vo);
    }

    @Override
    public List<UrbUdgdSnkgHistVO> selectUrbUdgdSnkgHistList(UrbUdgdSnkgHistVO vo) throws Exception {
        return urbUdgdSnkgHistMapper.selectUrbUdgdSnkgHistList(vo);
    }

    @Override
    public int selectUrbUdgdSnkgHistListCnt(UrbUdgdSnkgHistVO vo) throws Exception {
        return urbUdgdSnkgHistMapper.selectUrbUdgdSnkgHistListCnt(vo);
    }
    
    @Override
    public int insertUrbUdgdSnkgHist(UrbUdgdSnkgHistVO vo) throws Exception {
        urbUdgdSnkgHistMapper.insertUrbUdgdSnkgHist(vo);
        return vo.getAcdntNo(); // 변경된 PK Getter 사용 (keyProperty="acdntNo")
    }

//    @Override
//    public int updateUrbUdgdSnkgHist(UrbUdgdSnkgHistVO vo) throws Exception {
//        urbUdgdSnkgHistMapper.updateUrbUdgdSnkgHist(vo);
//        return 1;
//    }
//
//    @Override
//    public int deleteUrbUdgdSnkgHist(UrbUdgdSnkgHistVO vo) throws Exception {
//        urbUdgdSnkgHistMapper.deleteUrbUdgdSnkgHist(vo);
//        return 1;
//    }
    
    @Override
    public List<UrbUdgdSnkgOpenApiListResDTO> fetchAllIncheonSubsidenceDtlList(UrbUdgdSnkgOpenApiReqDTO reqDto) throws Exception {
        
        List<UrbUdgdSnkgOpenApiListResDTO> finalResultList = new ArrayList<>();
        
        // 1. [DB 조회] 대상지(zoneNo)에 해당하는 '시군구'와 '읍면동' 조회
        String targetSigungu = null;
        String targetDong = null;
        
        if (reqDto.getZoneNo() != null && reqDto.getZoneNo() > 0) {
            targetSigungu = dssCommonMapper.selectSigunguNmByZoneNo(reqDto.getZoneNo());
            targetDong = dssCommonMapper.selectDongNmByZoneNo(reqDto.getZoneNo()); 
            log.info("Target Zone Info: Sigungu={}, Dong={}", targetSigungu, targetDong);
        }

        // 2. API 페이징 루프 설정
        int numOfRows = 100;
        int currentPage = 1;
        int totalApiCount = 0;
        
        // DTO에 페이징 정보 강제 주입
        reqDto.setNumOfRows(numOfRows);
        
        boolean hasMoreData = true;

        while (hasMoreData) {
            reqDto.setPageNo(currentPage);
            
            // 2-1. 목록 API URI 생성
            URI listUri = UriComponentsBuilder.fromHttpUrl(baseUrlList)
                    .queryParam("serviceKey", serviceKey)
                    .queryParam("pageNo", reqDto.getPageNo())
                    .queryParam("numOfRows", reqDto.getNumOfRows())
                    .queryParam("type", "json")
                    .queryParam("sagoDateFrom", reqDto.getSagoDateFrom())
                    .queryParam("sagoDateTo", reqDto.getSagoDateTo())
                    .build(true).toUri();
            
            try {
                // 2-2. API 호출 및 파싱
                String listResBody = restTemplate.getForObject(listUri, String.class);
                JsonNode root = objectMapper.readTree(listResBody);
                
                // Total Count 체크 (첫 페이지에서만 수행)
                if (currentPage == 1) {
                    if (root.path("response").has("totalCount")) {
                        totalApiCount = root.path("response").path("totalCount").asInt(0);
                    } else {
                        // 구조가 다를 경우 대비 (header/body)
                        totalApiCount = root.path("response").path("body").path("totalCount").asInt(0);
                    }
                    
                    if (totalApiCount == 0) {
                        hasMoreData = false;
                        break;
                    }
                }

                // Items 노드 파싱
                JsonNode itemsNode = root.path("response").path("body").path("items");
                List<UrbUdgdSnkgOpenApiListResDTO> batchList = new ArrayList<>();
                
                if (itemsNode.isArray()) {
                    batchList = objectMapper.convertValue(itemsNode, new TypeReference<List<UrbUdgdSnkgOpenApiListResDTO>>() {});
                } else if (!itemsNode.isMissingNode() && !itemsNode.isNull()) {
                    // 단건일 경우 객체로 올 수 있음
                    batchList.add(objectMapper.convertValue(itemsNode, UrbUdgdSnkgOpenApiListResDTO.class));
                }

                // 3. [1차 필터링] 시군구 레벨 필터링
                // 람다 내부 사용을 위해 final 변수 할당
                String finalTargetSigungu = targetSigungu;
                
                List<UrbUdgdSnkgOpenApiListResDTO> sigunguFilteredBatch = batchList.stream()
                    .filter(item -> {
                        // 조건 A: 인천광역시 데이터인지 확인
                        boolean isIncheon = item.getSido() != null && item.getSido().contains("인천");
                        
                        // 조건 B: 대상지 시군구가 지정된 경우 일치 여부 확인
                        boolean isTargetGun = true;
                        if (finalTargetSigungu != null && !finalTargetSigungu.isEmpty()) {
                            isTargetGun = item.getSigungu() != null && item.getSigungu().trim().equals(finalTargetSigungu.trim());
                        }
                        
                        return isIncheon && isTargetGun;
                    })
                    .toList();

                // 4. [상세 조회 및 2차 필터링] 동(Dong) 레벨 확인
                String finalTargetDong = targetDong;
                
                // 병렬 스트림 대신 순차 처리 혹은 병렬 처리 (여기선 명시적 반복문 사용)
                for (UrbUdgdSnkgOpenApiListResDTO item : sigunguFilteredBatch) {
                    
                    // A. 상세 정보 호출 (여기서 dong, addr 등이 채워짐)
                    fillDetailInfo(item);
                    
                    // B. 동(Dong) 일치 여부 확인 후 결과 리스트에 추가
                    // 대상지 동 정보가 있을 때만 검사, 없으면(null) 시군구 통과한 것 모두 추가
                    if (finalTargetDong != null && !finalTargetDong.isEmpty()) {
                        boolean matchDong = false;
                        
                        // 확인 1: API의 'dong' 필드에 포함되는지
                        if (item.getDong() != null && item.getDong().contains(finalTargetDong)) {
                            matchDong = true;
                        }
                        // 확인 2: API의 'addr' 필드에 포함되는지 (동 정보가 주소에 섞여있는 경우)
                        else if (item.getAddr() != null && item.getAddr().contains(finalTargetDong)) {
                            matchDong = true;
                        }
                        
                        if (matchDong) {
                            finalResultList.add(item);
                        }
                    } else {
                        // 동 정보가 없으면 시군구 필터링 된 것 그대로 추가
                        finalResultList.add(item);
                    }
                }

                // 페이징 탈출 조건 확인
                int fetchedSoFar = currentPage * numOfRows;
                if (fetchedSoFar >= totalApiCount || batchList.isEmpty()) {
                    hasMoreData = false;
                } else {
                    currentPage++;
                }

            } catch (Exception e) {
            	log.error("API 데이터 수집 중 페이징 처리 오류 발생 (페이지: {})", currentPage);
                log.debug("Paging Loop Error", e);
                // 에러 발생 시 루프 중단 (혹은 continue로 다음 페이지 시도 가능)
                hasMoreData = false;
            }
        }

        log.info("Finished. Final Count: {}", finalResultList.size());
        return finalResultList;
    }

    /**
     * [Helper] 상세 API 호출하여 DTO에 데이터 병합
     */
    private void fillDetailInfo(UrbUdgdSnkgOpenApiListResDTO item) {
        try {
            URI detailUri = UriComponentsBuilder.fromHttpUrl(baseUrlDetail)
                    .queryParam("serviceKey", serviceKey)
                    .queryParam("type", "json")
                    .queryParam("sagoNo", item.getSagoNo())
                    .build(true).toUri();

            String detailResBody = restTemplate.getForObject(detailUri, String.class);
            JsonNode root = objectMapper.readTree(detailResBody);
            JsonNode itemsNode = root.path("response").path("body").path("items");

            if (itemsNode.isArray() && itemsNode.size() > 0) {
                JsonNode detail = itemsNode.get(0);
                
                // DTO 필드 매핑
                item.setDong(detail.path("dong").asText(null));
                item.setAddr(detail.path("addr").asText(null));
                item.setSagoDetail(detail.path("sagoDetail").asText(null));
                item.setTrStatus(detail.path("trStatus").asText(null));
                item.setTrMethod(detail.path("trMethod").asText(null));
                item.setTrAmt(detail.path("trAmt").asText(null));
                item.setGrdKind(detail.path("grdKind").asText(null));
                item.setTrFnDate(detail.path("trFnDate").asText(null));
                
                // 피해 현황
                item.setDeathCnt(detail.path("deathCnt").asText("0"));
                item.setInjuryCnt(detail.path("injuryCnt").asText("0"));
                item.setVehicleCnt(detail.path("vehicleCnt").asText("0"));

                // 좌표 (없으면 0.0)
                item.setSagoLat(new BigDecimal(detail.path("sagoLat").asText("0.0")));
                item.setSagoLon(new BigDecimal(detail.path("sagoLon").asText("0.0")));

                // 숫자형 데이터
                item.setSinkWidth(new BigDecimal(detail.path("sinkWidth").asText("0")));
                item.setSinkExtend(new BigDecimal(detail.path("sinkExtend").asText("0")));
                item.setSinkDepth(new BigDecimal(detail.path("sinkDepth").asText("0")));
            }
        } catch (Exception e) {
        	log.warn("외부 API 상세 조회 실패 - 사고번호: {}", item.getSagoNo());
            log.debug("API Call Error", e);
        }
    }

    /**
     * 인천광역시(+특정 시군구) 데이터 전수 조회
     * 1. zoneNo가 있으면 DB에서 해당 구역의 '시군구명' 조회
     * 2. API를 루프 돌면서 전체 데이터를 가져옴
     * 3. '인천광역시' AND '해당 시군구' 데이터만 필터링해서 반환
     */
    @Override
    public List<UrbUdgdSnkgOpenApiListResDTO> fetchAllIncheonSubsidenceList(UrbUdgdSnkgOpenApiReqDTO reqDto) throws Exception {
        
        List<UrbUdgdSnkgOpenApiListResDTO> finalResultList = new ArrayList<>();
        
        // 1. [DB 조회] 대상지(zoneNo)가 있다면 해당 시군구 이름(예: "중구") 조회
        String targetSigungu = null;
        if (reqDto.getZoneNo() != null && reqDto.getZoneNo() > 0) {
            targetSigungu = dssCommonMapper.selectSigunguNmByZoneNo(reqDto.getZoneNo());
            log.warn("▶ Target Zone({}) belongs to Sigungu: {}", reqDto.getZoneNo(), targetSigungu);
        }

        // 2. [API 설정] 루프 초기값 세팅
        int numOfRows = 100; // 한 번에 100개씩 조회 (최대치 권장)
        int currentPage = 1;
        int totalApiCount = 0; // API 전체 데이터 수
        boolean hasMoreData = true;

        // DTO에 강제 세팅 (API 호출용)
        reqDto.setNumOfRows(numOfRows);

        // 3. [Loop] 전체 데이터 순회 시작
        while (hasMoreData) {
            reqDto.setPageNo(currentPage);
            
            // 3-1. URI 생성
            URI uri = UriComponentsBuilder.fromHttpUrl(baseUrlList)
                    .queryParam("serviceKey", serviceKey)
                    .queryParam("pageNo", reqDto.getPageNo())
                    .queryParam("numOfRows", reqDto.getNumOfRows())
                    .queryParam("type", "json")
                    .queryParam("sagoDateFrom", reqDto.getSagoDateFrom())
                    .queryParam("sagoDateTo", reqDto.getSagoDateTo())
                    .build(true) // 인코딩 방지
                    .toUri();

            log.debug("API Fetching Page {}: {}", currentPage, uri);

            try {
                // 3-2. API 호출
                String responseBody = restTemplate.getForObject(uri, String.class);
                JsonNode root = objectMapper.readTree(responseBody);

                // 3-3. Total Count 파싱 (첫 페이지일 때만 확인)
                if (currentPage == 1) {
                    JsonNode headerNode = root.path("response").path("header");
                    if (!"0".equals(headerNode.path("resultCode").asText("0"))) {
                        log.error("API Error: {}", headerNode.path("resultMsg").asText());
                        break;
                    }
                    
                    // totalCount 위치 확인 (body 안인지 밖인지 체크)
                    JsonNode bodyNode = root.path("response").path("body");
                    if (bodyNode.has("totalCount")) {
                        totalApiCount = bodyNode.path("totalCount").asInt(0);
                    } else if (root.path("response").has("totalCount")) {
                        totalApiCount = root.path("response").path("totalCount").asInt(0);
                    }
                    log.warn("▶ Total Count to fetch from API: {}", totalApiCount);
                    
                    if (totalApiCount == 0) break; // 데이터 없으면 종료
                }

                // 3-4. Items 파싱
                JsonNode itemsNode = root.path("response").path("body").path("items");
                List<UrbUdgdSnkgOpenApiListResDTO> currentBatch = new ArrayList<>();

                if (itemsNode.isArray()) {
                    currentBatch = objectMapper.convertValue(itemsNode, new TypeReference<List<UrbUdgdSnkgOpenApiListResDTO>>() {});
                } else if (!itemsNode.isMissingNode() && !itemsNode.isNull()) {
                    // 배열이 아닌 단건 객체로 오는 경우 대비
                    currentBatch.add(objectMapper.convertValue(itemsNode, UrbUdgdSnkgOpenApiListResDTO.class));
                }

                // 3-5. [필터링 로직] 인천광역시 + 시군구
                String finalTargetSigungu = targetSigungu; // 람다 내부 사용용

                List<UrbUdgdSnkgOpenApiListResDTO> filtered = currentBatch.stream()
                    .filter(item -> {
                        // 조건 1: 시도가 '인천'을 포함해야 함 (인천광역시)
                        boolean isIncheon = item.getSido() != null && item.getSido().contains("인천");
                        
                        // 조건 2: 대상지 시군구가 있다면 일치해야 함 (없으면 통과)
                        boolean isTargetGun = true;
                        if (finalTargetSigungu != null && !finalTargetSigungu.isEmpty()) {
                            isTargetGun = item.getSigungu() != null && item.getSigungu().trim().equals(finalTargetSigungu.trim());
                        }
                        
                        return isIncheon && isTargetGun;
                    })
                    .toList();

                finalResultList.addAll(filtered);

                // 가져온 데이터가 없거나, 현재까지 조회한 건수가 전체 건수보다 크거나 같으면 종료
                int fetchedCount = currentPage * numOfRows;
                if (currentBatch.isEmpty() || fetchedCount >= totalApiCount) {
                    hasMoreData = false;
                } else {
                    currentPage++; // 다음 페이지로
                }

            } catch (Exception e) {
                log.error("API Call Failed at Page {}", currentPage, e);
                hasMoreData = false; // 에러 나면 중단
            }
        }

        log.info("▶ Fetch Finished. Filtered {} items (Incheon/{}).", finalResultList.size(), targetSigungu != null ? targetSigungu : "All");
        return finalResultList;
    }

    // -------------------------------------------------------------------------
    // 1. Open API 목록 조회 구현
    // -------------------------------------------------------------------------
    @Override
    public List<UrbUdgdSnkgOpenApiListResDTO> getOpenApiSubsidenceList(UrbUdgdSnkgOpenApiReqDTO reqDto) throws Exception {
        
        URI uri = UriComponentsBuilder.fromHttpUrl(baseUrlList)
                .queryParam("serviceKey", serviceKey)
                .queryParam("pageNo", reqDto.getPageNo())
                .queryParam("numOfRows", reqDto.getNumOfRows())
                .queryParam("type", "json") 
                .queryParam("sagoDateFrom", reqDto.getSagoDateFrom())
                .queryParam("sagoDateTo", reqDto.getSagoDateTo())
                .build(true) 
                .toUri();
        
//        log.info("API List Req: {}", uri);

        String responseBody = restTemplate.getForObject(uri, String.class);
        
        JsonNode root = objectMapper.readTree(responseBody);
        
        // 경로: response -> body -> items
        JsonNode itemsNode = root.path("response").path("body").path("items");

        if (itemsNode.isMissingNode() || itemsNode.isNull()) {
            return Collections.emptyList();
        }

        if (itemsNode.isArray()) {
            // items가 배열인 경우 바로 리스트로 변환
            return objectMapper.convertValue(itemsNode, new TypeReference<List<UrbUdgdSnkgOpenApiListResDTO>>() {});
        } else {
            // (혹시 모를 예외) 배열이 아니라 객체로 올 경우
            List<UrbUdgdSnkgOpenApiListResDTO> list = new ArrayList<>();
            list.add(objectMapper.convertValue(itemsNode, UrbUdgdSnkgOpenApiListResDTO.class));
            return list;
        }
    }

    // -------------------------------------------------------------------------
    // 2. Open API 상세 조회 구현
    // -------------------------------------------------------------------------
    @Override
    public UrbUdgdSnkgOpenApiDetailResDTO getOpenApiSubsidenceDetail(String sagoNo) throws Exception {
        
        URI uri = UriComponentsBuilder.fromHttpUrl(baseUrlDetail)
                .queryParam("serviceKey", serviceKey)
//                .queryParam("pageNo", "1") // 상세 조회 API에는 pageNo, numOfRows가 필요 없거나 1로 고정
//                .queryParam("numOfRows", "1")
                .queryParam("type", "json")
                .queryParam("sagoNo", sagoNo)
                .build(true)
                .toUri();

        log.info("API Detail Req: {}", uri);

        String responseBody = restTemplate.getForObject(uri, String.class);
        JsonNode root = objectMapper.readTree(responseBody);
        
        // [수정] items가 배열임. 상세조회지만 리스트([]) 형태로 내려옴
        JsonNode itemsNode = root.path("response").path("body").path("items");

        if (itemsNode.isArray() && itemsNode.size() > 0) {
            // 배열의 첫 번째 요소(0번 인덱스)를 DTO로 변환
            return objectMapper.convertValue(itemsNode.get(0), UrbUdgdSnkgOpenApiDetailResDTO.class);
        }

        return null;
    }
    
    /**
     * [BATCH Implementation]
     * API 전수 조사 -> DTO를 VO로 변환 -> DB Upsert(중복 시 업데이트)
     */
    @Override
    @Transactional
    public void syncIncheonSubsidenceData() throws Exception {
        log.info(">>> [BATCH] 지반침하 사고 데이터 동기화 시작");

        UrbUdgdSnkgOpenApiReqDTO reqDto = new UrbUdgdSnkgOpenApiReqDTO();
        // 데이터 적재 시에는 과거 누락분 포함을 위해 조회 범위를 넓게 설정
        reqDto.setSagoDateFrom("20100101");
        reqDto.setSagoDateTo(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")));

        // 1. 기존에 작성한 API 전수 조사 로직 호출 (상세 데이터 포함)
        List<UrbUdgdSnkgOpenApiListResDTO> apiItems = this.fetchAllIncheonSubsidenceDtlList(reqDto);

        int count = 0;
        for (UrbUdgdSnkgOpenApiListResDTO item : apiItems) {
            // 2. DTO -> VO 매핑 (제공해주신 DB 테이블 컬럼명 기준)
            UrbUdgdSnkgHistVO vo = convertDtoToVo(item);
            
            // 3. DB 적재 (PostgreSQL ON CONFLICT 구문 활용)
            urbUdgdSnkgHistMapper.upsertUrbUdgdSnkgHist(vo);
            count++;
        }
        log.info(">>> [BATCH] 동기화 완료. 총 {}건 처리.", count);
    }

    private UrbUdgdSnkgHistVO convertDtoToVo(UrbUdgdSnkgOpenApiListResDTO dto) {
        UrbUdgdSnkgHistVO vo = new UrbUdgdSnkgHistVO();
        
        // 1. 기본 문자열/정수 매핑
        vo.setAcdntNo(Integer.parseInt(dto.getSagoNo()));
        vo.setAcdntCtpvNm(dto.getSido());
        vo.setAcdntSggNm(dto.getSigungu());
        vo.setAcdntEmVina(dto.getDong());
        vo.setAcdntDaddr(dto.getAddr());
        
        // 2. 좌표 매핑 (BigDecimal -> Double 변환)
        // .doubleValue()를 사용하여 변환합니다. Null 체크를 포함하면 안전합니다.
        if (dto.getSagoLat() != null) vo.setAcdntBrnchLat(dto.getSagoLat().doubleValue());
        if (dto.getSagoLon() != null) vo.setAcdntBrnchLot(dto.getSagoLon().doubleValue());
        
        // 3. 일자 매핑 (YYYY-MM-DD 또는 YYYYMMDD 대응)
        String sagoDate = dto.getSagoDate();
        vo.setOcrnYmd(sagoDate != null ? sagoDate.replace("-", "") : "");
        
        // 4. 규모 매핑 (BigDecimal -> Double 변환)
        if (dto.getSinkWidth() != null) vo.setOcrnSclBt(dto.getSinkWidth().doubleValue());
        if (dto.getSinkExtend() != null) vo.setOcrnSclPrlg(dto.getSinkExtend().doubleValue());
        if (dto.getSinkDepth() != null) vo.setOcrnSclDepth(dto.getSinkDepth().doubleValue());
        
        // 5. 기타 정보
        vo.setOcrnRgnNtrfsKnd(dto.getGrdKind());
        vo.setDtlOcrnCs(dto.getSagoDetail());
        
        // 6. 피해 건수 매핑 (String -> Integer 변환)
        // DTO의 deathCnt 등이 String이므로 Integer.valueOf() 처리가 필요합니다.
        vo.setDamDcsdCnt(parseSafeInt(dto.getDeathCnt()));
        vo.setDamInjpsnCnt(parseSafeInt(dto.getInjuryCnt()));
        vo.setDamVhclCntom(parseSafeInt(dto.getVehicleCnt()));
        
        // 7. 복구 정보
        vo.setRstrStts(dto.getTrStatus());
        vo.setRstrMthd(dto.getTrMethod());
        
        // 복구비용 (DTO의 trAmt가 String이면 Double로 변환)
        if (dto.getTrAmt() != null && !dto.getTrAmt().isEmpty()) {
            try {
                vo.setRstrCst(Double.parseDouble(dto.getTrAmt()));
            } catch (NumberFormatException e) {
                vo.setRstrCst(0.0);
            }
        }
        
        vo.setRstrCmptnYmd(dto.getTrFnDate());
        
        // 8. 시스템 필드
        vo.setFrstRegId("SYSTEM");
        vo.setLastMdfcnId("SYSTEM");
        
        return vo;
    }

    /** 정수 파싱 안전 유틸 */
    private Integer parseSafeInt(String val) {
        if (val == null || val.trim().isEmpty()) return 0;
        try {
            return Integer.parseInt(val.replaceAll("[^0-9]", "")); // 숫자 외 문자 제거 후 변환
        } catch (Exception e) {
        	log.trace("정수 변환 실패 (입력값: {}). 기본값 0을 반환합니다.", val);
            return 0;
        }
    }
    
    /**
     * [Service Implementation] 
     * DB에서 조회하되, 기존 API 방식과 동일하게 '동(Dong)' 단위까지 정밀 필터링 수행
     */
    @Override
    public List<UrbUdgdSnkgOpenApiListResDTO> selectIncheonSubsidenceDbList(UrbUdgdSnkgOpenApiReqDTO reqDto) throws Exception {
        
        UrbUdgdSnkgHistVO searchVO = new UrbUdgdSnkgHistVO();
        
        // 1. [파라미터 매핑] 날짜
        if (reqDto.getSagoDateFrom() != null) {
            searchVO.setOcrnYmdStart(reqDto.getSagoDateFrom().replace(".", "").replace("-", ""));
        }
        if (reqDto.getSagoDateTo() != null) {
            searchVO.setOcrnYmdEnd(reqDto.getSagoDateTo().replace(".", "").replace("-", ""));
        }

        // 2. [DB 조회] 대상지(zoneNo)에 해당하는 '시군구' 가져오기
        String targetSigungu = null;
//        String targetDong = null;
        if (reqDto.getZoneNo() != null && reqDto.getZoneNo() > 0) {
            targetSigungu = dssCommonMapper.selectSigunguNmByZoneNo(reqDto.getZoneNo());
//            targetDong = dssCommonMapper.selectDongNmByZoneNo(reqDto.getZoneNo());
            
            // 시군구는 1차 필터링을 위해 VO에 세팅 (SQL에서 처리)
            if (targetSigungu != null) {
                searchVO.setAcdntSggNm(targetSigungu.trim());
            }
        }
        
        // 전수 조회를 위해 페이징 크게 설정
        searchVO.setFirstIndex(0);
        searchVO.setRecordCountPerPage(9999); 

        // 3. [DB 조회] 1차 필터링된 결과 (시군구 레벨)
        List<UrbUdgdSnkgHistVO> dbList = urbUdgdSnkgHistMapper.selectUrbUdgdSnkgHistList(searchVO);

        // 4. [2차 필터링] 읍면동(Dong) 일치 여부 확인 (기존 로직과 동일)
        List<UrbUdgdSnkgOpenApiListResDTO> finalResultList = new ArrayList<>();
//        String finalTargetDong = (targetDong != null) ? targetDong.trim() : null;

        for (UrbUdgdSnkgHistVO vo : dbList) {
//            boolean isMatch = false;

//            // 대상지 동 정보가 없으면 시군구 필터만 통과하면 OK
//            if (finalTargetDong == null || finalTargetDong.isEmpty()) {
//                isMatch = true;
//            } else {
//                // 기존 로직: API의 'dong' 필드 또는 'addr' 필드에 대상지 동 명칭이 포함되는지 확인
//                String voDong = (vo.getAcdntEmVina() != null) ? vo.getAcdntEmVina() : "";
//                String voAddr = (vo.getAcdntDaddr() != null) ? vo.getAcdntDaddr() : "";
//
//                if (voDong.contains(finalTargetDong) || voAddr.contains(finalTargetDong)) {
//                    isMatch = true;
//                }
//            }
//
//            if (isMatch) {
                finalResultList.add(convertVoToDto(vo));
//            }
        }

//        log.info(">>> Finished DB Search. TargetDong: {}, Final Count: {}", finalTargetDong, finalResultList.size());
        log.info(">>> Finished DB Search. TargetSigungu: {}, Final Count: {}", targetSigungu, finalResultList.size());
        return finalResultList;
    }

    /** [Helper] VO를 DTO로 변환하여 기존 프론트엔드 규격 유지 */
    private UrbUdgdSnkgOpenApiListResDTO convertVoToDto(UrbUdgdSnkgHistVO vo) {
        UrbUdgdSnkgOpenApiListResDTO dto = new UrbUdgdSnkgOpenApiListResDTO();
        
        dto.setSagoNo(String.valueOf(vo.getAcdntNo()));
        dto.setSido(vo.getAcdntCtpvNm());
        dto.setSigungu(vo.getAcdntSggNm());
        dto.setDong(vo.getAcdntEmVina());
        dto.setAddr(vo.getAcdntDaddr());
        dto.setSagoDate(vo.getOcrnYmd()); 
        
        // Double -> BigDecimal 변환
        if (vo.getAcdntBrnchLat() != null) dto.setSagoLat(BigDecimal.valueOf(vo.getAcdntBrnchLat()));
        if (vo.getAcdntBrnchLot() != null) dto.setSagoLon(BigDecimal.valueOf(vo.getAcdntBrnchLot()));
        if (vo.getOcrnSclBt() != null) dto.setSinkWidth(BigDecimal.valueOf(vo.getOcrnSclBt()));
        if (vo.getOcrnSclPrlg() != null) dto.setSinkExtend(BigDecimal.valueOf(vo.getOcrnSclPrlg()));
        if (vo.getOcrnSclDepth() != null) dto.setSinkDepth(BigDecimal.valueOf(vo.getOcrnSclDepth()));
        
        dto.setGrdKind(vo.getOcrnRgnNtrfsKnd());
        dto.setSagoDetail(vo.getDtlOcrnCs());
        dto.setTrStatus(vo.getRstrStts());
        dto.setTrMethod(vo.getRstrMthd());
        dto.setTrFnDate(vo.getRstrCmptnYmd());
        
        // [수정] null 체크를 강화하고 기본값 "0"을 확실히 할당
        dto.setDeathCnt(vo.getDamDcsdCnt() != null ? String.valueOf(vo.getDamDcsdCnt()) : "0");
        dto.setInjuryCnt(vo.getDamInjpsnCnt() != null ? String.valueOf(vo.getDamInjpsnCnt()) : "0");
        dto.setVehicleCnt(vo.getDamVhclCntom() != null ? String.valueOf(vo.getDamVhclCntom()) : "0");
        
        return dto;
    }
}