package incheon.ags.mrb.style.service.impl;

import com.all4land.sa.util.ClassificationUtils;
import com.fasterxml.jackson.databind.ObjectMapper;

import incheon.ags.mrb.main.mapper.RecipeLayerMapper;
import incheon.ags.mrb.style.mapper.RecipeLayerStyleMapper;
import incheon.ags.mrb.style.service.RecipeLayerStyleService;
import incheon.ags.mrb.style.utils.StyleConverter;
import incheon.ags.mrb.style.utils.StyleJsonConverter;
import incheon.ags.mrb.style.vo.JsonStyleVO;
import incheon.ags.mrb.style.vo.RecipeLayerStyleVO;
import incheon.ags.mrb.style.web.dto.CategoryValueDTO;
import incheon.ags.mrb.style.web.dto.ClassificationRequestDTO;
import incheon.ags.mrb.style.web.dto.RecipeStyleDTO;
import incheon.ags.mrb.style.web.dto.RecipeStyleSearchDTO;
import incheon.cmm.g2f.layer.domain.LayerType;
import incheon.com.cmm.service.ResultVO;
import lombok.extern.slf4j.Slf4j;
import org.geotools.api.data.Query;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.filter.Filter;
import org.geotools.data.postgis.PostgisNGDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.jdbc.JDBCDataStore;
import org.jaitools.numeric.Range;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 레시피 레이어 스타일 Service 구현 클래스
 * - 레시피 레이어 스타일 정보 관리 서비스 구현
 */
@Slf4j
@Service
@Transactional(readOnly = true)
public class RecipeLayerStyleServiceImpl implements RecipeLayerStyleService {

    private static final int MAX_CATEGORY_VALUES = 1000;

    private final RecipeLayerStyleMapper recipeLayerStyleMapper;
    private final RecipeLayerMapper recipeLayerMapper;
    private final RestTemplate restTemplate;
    private final DataSource dataSource;
    private final ObjectMapper objectMapper;

    RecipeLayerStyleServiceImpl(RecipeLayerStyleMapper recipeLayerStyleMapper, RecipeLayerMapper recipeLayerMapper,
            RestTemplate restTemplate, @Qualifier("dataSource") DataSource dataSource, ObjectMapper objectMapper) {
        this.recipeLayerStyleMapper = recipeLayerStyleMapper;
        this.recipeLayerMapper = recipeLayerMapper;
        this.restTemplate = restTemplate;
        this.dataSource = dataSource;
        this.objectMapper = objectMapper;
    }

    @Resource
    private StyleConverter styleConverter;

    @Resource
    private StyleJsonConverter styleJsonConverter;

    @Value("${gis.server.url:http://10.100.232.241:3004/MapPrimeServer}")
    private String mapprimeServerUrl;

    @Value("${gis.server.prefix:incheon}")
    private String mapprimeWorkspace;

    @Override
    public List<RecipeStyleDTO> selectServerStyleList(RecipeStyleSearchDTO searchDTO, String userId) throws Exception {
        log.debug("레시피 레이어 스타일 목록 조회 - 검색조건: {}, userId: {}", searchDTO, userId);

        // 페이징 정보 초기화
        if (searchDTO != null) {
            searchDTO.initializePaging();
        }

        // Mapper 호출 (userId 별도 전달)
        List<RecipeLayerStyleVO> voList = recipeLayerStyleMapper.selectServerStyleList(searchDTO, userId);

        // VO 목록을 RecipeStyleDTO 목록으로 변환 (작성자/수정자 정보 제외)
        return convertToRecipeStyleDTOList(voList);
    }

    @Override
    public int selectServerStyleListCount(RecipeStyleSearchDTO searchDTO, String userId) throws Exception {
        log.debug("레시피 레이어 스타일 총 개수 조회 - 검색조건: {}, userId: {}", searchDTO, userId);

        return recipeLayerStyleMapper.selectServerStyleListCount(searchDTO, userId);
    }

    @Override
    public RecipeStyleDTO selectServerStyle(Long styleId) throws Exception {
        log.debug("레시피 레이어 스타일 상세 조회 - styleId: {}", styleId);

        if (styleId == null) {
            throw new IllegalArgumentException("스타일 ID는 필수입니다.");
        }

        RecipeLayerStyleVO result = recipeLayerStyleMapper.selectServerStyle(styleId);
        if (result == null) {
            throw new IllegalArgumentException("해당 스타일을 찾을 수 없습니다. styleId: " + styleId);
        }

        // MapPrime 서버에 스타일이 실제로 존재하는지 확인 (styleSrvcNm이 있는 경우만)
        if (result.getStyleSrvcNm() != null && !result.getStyleSrvcNm().isEmpty()) {
            boolean exists = existsMapprimeStyle(result.getStyleSrvcNm());
            if (!exists) {
                log.error("MapPrime 서버에 스타일이 존재하지 않음 - styleId: {}, styleSrvcNm: {}",
                        result.getStyleId(), result.getStyleSrvcNm());
                throw new IllegalStateException(
                        String.format("MapPrime 서버에 스타일이 존재하지 않습니다. styleId: %d, styleSrvcNm: %s",
                                result.getStyleId(), result.getStyleSrvcNm()));
            }
        }

        // VO를 RecipeStyleDTO로 변환 (작성자/수정자 정보 제외)
        return convertToRecipeStyleDTO(result);
    }

    @Override
    public List<RecipeStyleDTO> selectServerStyles(List<Long> styleIds) throws Exception {
        log.debug("레시피 레이어 스타일 배치 조회 - styleIds size: {}", styleIds != null ? styleIds.size() : 0);

        if (styleIds == null || styleIds.isEmpty()) {
            return Collections.emptyList();
        }

        List<Long> filteredStyleIds = styleIds.stream()
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());

        if (filteredStyleIds.isEmpty()) {
            return Collections.emptyList();
        }

        List<RecipeLayerStyleVO> styleVOList = recipeLayerStyleMapper.selectServerStyles(filteredStyleIds);
        return convertToRecipeStyleDTOList(styleVOList);
    }

    /**
     * 레이어 스타일 등록
     *
     * 처리 순서:
     * 1. JSON 스타일을 SLD(Styled Layer Descriptor) XML로 변환
     * 2. UUID 기반 고유 스타일명 생성 (style_xxxxxxxxxxxx 형식)
     * 3. MapPrime 서버에 POST 요청으로 스타일 저장
     * 4. MapPrime 저장 성공 시 DB에 스타일 정보 저장 (styleSrvcNm에 MapPrime 스타일명 저장)
     *
     * @param jsonStyleVO JSON 형식의 스타일 정의 객체
     * @param userId      현재 로그인한 사용자 ID
     * @return ResultVO 등록 결과 (성공: code=0, 실패: code=-1)
     * @throws Exception JSON→SLD 변환 실패, MapPrime API 호출 실패, DB 저장 실패 등
     */
    @Override
    @Transactional
    public ResultVO insertLayerStyle(JsonStyleVO jsonStyleVO, String userId) {
        log.debug("레이어 스타일 등록 - layerId: {}, userId: {}", jsonStyleVO.getLayerId(), userId);

        try {
            // 0. 스타일명 중복 체크
            RecipeStyleDTO styleDTO = convertJsonStyleVOToRecipeStyleDTO(jsonStyleVO);
            String styleNm = styleDTO.getStyleNm();

            // wrtrId와 styleNm 조합으로 중복 확인
            RecipeLayerStyleVO existingStyle = recipeLayerStyleMapper.selectStyleByUserIdAndName(userId, styleNm);
            if (existingStyle != null) {
                log.warn("스타일명 중복 - userId: {}, styleNm: {}", userId, styleNm);
                ResultVO resultVO = new ResultVO();
                resultVO.setResultCode(-1);
                resultVO.setResultMessage("이미 사용 중인 스타일명입니다. 다른 이름을 사용해주세요.");
                return resultVO;
            }

            // 1. JSON을 SLD(XML)로 변환
            log.debug("JSON을 SLD로 변환 시작");
            jsonStyleVO.setExportedAt(LocalDateTime.now());
            String sldContent = styleConverter.convertToSld(jsonStyleVO);
            log.debug("SLD 변환 완료 - 길이: {} 문자", sldContent.length());

            // 2. UUID 기반 고유 스타일명 생성
            String uniqueStyleName = "style_" + UUID.randomUUID().toString();
            log.debug("생성된 스타일명: {}", uniqueStyleName);

            // 3. MapPrime 서버에 스타일 저장
            String url = mapprimeServerUrl + "/rest/style";

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);

            // MapPrime API 요청 본문 생성
            Map<String, String> jsonBody = new HashMap<>();
            jsonBody.put("name", uniqueStyleName);
            jsonBody.put("xml", sldContent);
            jsonBody.put("workspace", mapprimeWorkspace);

            HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(jsonBody, headers);

            log.debug("MapPrime 스타일 생성 API 호출: {}", url);
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);

            if (!response.getStatusCode().is2xxSuccessful()) {
                throw new Exception("MapPrime 서버에 스타일 저장 실패: " + response.getStatusCode());
            }

            log.info("MapPrime 서버에 스타일 저장 성공 - 스타일명: {}", uniqueStyleName);

            // 4. JsonStyleVO를 RecipeStyleDTO로 변환
            RecipeStyleDTO recipeStyleDTO = convertJsonStyleVOToRecipeStyleDTO(jsonStyleVO);

            // 5. iconId 추출하여 JSON 문자열로 변환
            Map<String, Object> iconInfoMap = extractIconIdMap(jsonStyleVO);
            String iconInfoJson = null;
            if (!iconInfoMap.isEmpty()) {
                iconInfoJson = objectMapper.writeValueAsString(iconInfoMap);
                log.debug("iconId Map 저장: {}", iconInfoJson);
            }

            // 6. DB에 스타일 정보 저장 (styleSrvcNm에 MapPrime 스타일명 저장)
            LocalDateTime now = LocalDateTime.now();
            RecipeLayerStyleVO serverStyleVO = RecipeLayerStyleVO.builder()
                    .wrtrId(userId)
                    .styleNm(recipeStyleDTO.getStyleNm())
                    .styleSrvcNm(uniqueStyleName) // MapPrime에 저장된 스타일명
                    .styleTypeCd(recipeStyleDTO.getStyleTypeCd())
                    .spceTy(recipeStyleDTO.getSpceTy())
                    .iconInfo(iconInfoJson) // 사용자 이미지 정보
                    .frstRegDt(now)
                    .lastMdfcnDt(now)
                    .frstRegId(userId)
                    .lastMdfcnId(userId)
                    .build();

            int result = recipeLayerStyleMapper.insertServerStyle(serverStyleVO);

            if (result > 0) {
                // MyBatis useGeneratedKeys로 자동 채워진 styleId 확인
                Long generatedStyleId = serverStyleVO.getStyleId();
                log.info("DB에 스타일 정보 저장 성공 - generatedStyleId: {}, styleSrvcNm: {}",
                        generatedStyleId, uniqueStyleName);

                // VO를 DTO로 변환 (작성자/수정자 정보 제외)
                RecipeStyleDTO responseDTO = convertToRecipeStyleDTO(serverStyleVO);

                // DTO 내용 로그 확인
                log.info("응답 DTO - styleId: {}, styleNm: {}, styleSrvcNm: {}",
                        responseDTO.getStyleId(), responseDTO.getStyleNm(), responseDTO.getStyleSrvcNm());

                ResultVO resultVO = new ResultVO();
                resultVO.setResultCode(0);
                resultVO.setResultMessage("스타일이 성공적으로 등록되었습니다.");
                resultVO.putResult("data", responseDTO); // DTO로 반환
                resultVO.putResult("styleSrvcNm", uniqueStyleName);
                resultVO.putResult("styleId", generatedStyleId); // styleId도 최상위에 추가

                log.info("최종 ResultVO - data: {}, styleId: {}, styleSrvcNm: {}",
                        responseDTO, generatedStyleId, uniqueStyleName);

                return resultVO;
            } else {
                throw new Exception("DB 저장 실패");
            }

        } catch (Exception e) {
            log.error("레이어 스타일 등록 실패 - layerId: {}, userId: {}", jsonStyleVO.getLayerId(), userId, e);
            ResultVO resultVO = new ResultVO();
            resultVO.setResultCode(-1);
            resultVO.setResultMessage("레이어 스타일 등록 실패 - layerId: " + jsonStyleVO.getLayerId() + ", userId: " + userId);
            return resultVO;
        }
    }

    /**
     * JsonStyleVO를 RecipeStyleDTO로 변환
     */
    private RecipeStyleDTO convertJsonStyleVOToRecipeStyleDTO(JsonStyleVO jsonStyleVO) {
        // 기본 정보 설정 - styleName이 있으면 사용, 없으면 layerName 사용
        String styleName = (jsonStyleVO.getStyleName() != null && !jsonStyleVO.getStyleName().isEmpty())
                ? jsonStyleVO.getStyleName()
                : jsonStyleVO.getLayerName();

        // 공간 타입 설정 (layerType을 spceTy로 매핑)
        String spceTy = jsonStyleVO.getLayerType().toUpperCase();

        // 스타일 타입 설정 (styleMode를 styleTypeCd로 매핑)
        String styleTypeCd = mapStyleModeToStyleTypeCd(jsonStyleVO.getStyleMode());

        // 스타일 서비스명 설정 (프런트에서 전달된 값 사용)
        String styleSrvcNm = (jsonStyleVO.getStyleSrvName() != null && !jsonStyleVO.getStyleSrvName().isEmpty())
                ? jsonStyleVO.getStyleSrvName()
                : null;

        if (styleSrvcNm != null) {
            log.debug("프런트에서 전달된 styleSrvcNm 설정: {}", styleSrvcNm);
        }

        return RecipeStyleDTO.builder()
                .styleNm(styleName)
                .spceTy(spceTy)
                .styleTypeCd(styleTypeCd)
                .styleSrvcNm(styleSrvcNm)
                .build();
    }

    /**
     * styleMode를 styleTypeCd로 매핑
     */
    private String mapStyleModeToStyleTypeCd(String styleMode) {
        switch (styleMode.toLowerCase()) {
            case "single":
                return "S";
            case "graduated":
                return "G";
            case "categorized":
                return "C";
            case "rule":
                return "R";
            default:
                return "S";
        }
    }

    /**
     * 레이어 스타일 수정
     *
     * 처리 순서:
     * 1. DB에서 기존 스타일 정보 조회 (존재 여부 확인)
     * 2. styleSrvcNm 존재 여부 확인 (MapPrime 스타일명 필수)
     * 3. JSON 스타일을 SLD XML로 변환
     * 4. MapPrime 서버에 PUT 요청으로 스타일 업데이트
     * 5. MapPrime 업데이트 성공 시 DB 업데이트
     *
     * @param styleId     수정할 스타일 ID
     * @param jsonStyleVO JSON 형식의 스타일 정의 객체
     * @param userId      현재 로그인한 사용자 ID
     * @return ResultVO 수정 결과 (성공: code=0, 실패: code=-1)
     * @throws Exception 스타일 없음, styleSrvcNm 없음, MapPrime API 호출 실패, DB 업데이트 실패 등
     */
    @Override
    @Transactional
    public ResultVO updateLayerStyle(Long styleId, JsonStyleVO jsonStyleVO, String userId) throws Exception {
        log.debug("레이어 스타일 수정 - styleId: {}, layerId: {}, userId: {}", styleId, jsonStyleVO.getLayerId(), userId);

        try {
            // 1. 기존 스타일 존재 확인
            RecipeLayerStyleVO existingStyle = recipeLayerStyleMapper.selectServerStyle(styleId);
            if (existingStyle == null) {
                ResultVO result = new ResultVO();
                result.setResultCode(-1);
                result.setResultMessage("수정할 스타일을 찾을 수 없습니다.");
                return result;
            }

            // [추가] 지정 스타일('D' 타입) 수정 차단
            if ("D".equalsIgnoreCase(existingStyle.getStyleTypeCd())) {
                log.warn("지정 스타일 수정 시도 차단 - styleId: {}, userId: {}", styleId, userId);
                ResultVO result = new ResultVO();
                result.setResultCode(-1);
                result.setResultMessage("지정 스타일(공용)은 수정할 수 없습니다.");
                return result;
            }

            // 2. 기존 styleSrvcNm 확인 (수정에는 필수)
            String styleSrvcNm = existingStyle.getStyleSrvcNm();
            if (styleSrvcNm == null || styleSrvcNm.isEmpty()) {
                ResultVO result = new ResultVO();
                result.setResultCode(-1);
                result.setResultMessage("MapPrime 스타일 정보가 없습니다. 스타일을 수정할 수 없습니다.");
                return result;
            }

            // 3. JSON을 SLD(XML)로 변환
            log.debug("JSON을 SLD로 변환 시작");
            jsonStyleVO.setExportedAt(LocalDateTime.now());
            String sldContent = styleConverter.convertToSld(jsonStyleVO);
            log.debug("SLD 변환 완료 - 길이: {} 문자", sldContent.length());

            // 4. MapPrime 서버에 스타일 업데이트
            log.debug("MapPrime 서버 스타일 업데이트 시작 - styleSrvcNm: {}", styleSrvcNm);
            String updateUrl = mapprimeServerUrl + "/rest/style/" + styleSrvcNm;

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);

            // MapPrime API 요청 본문 생성 (업데이트는 xml만 전송)
            Map<String, String> jsonBody = new HashMap<>();
            jsonBody.put("xml", sldContent);

            HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(jsonBody, headers);
            ResponseEntity<String> response = restTemplate.exchange(updateUrl, HttpMethod.PUT, requestEntity,
                    String.class);

            if (!response.getStatusCode().is2xxSuccessful()) {
                throw new Exception("MapPrime 서버에 스타일 업데이트 실패: " + response.getStatusCode());
            }
            log.info("MapPrime 서버 스타일 업데이트 성공 - styleSrvcNm: {}", styleSrvcNm);

            // 5. JsonStyleVO를 RecipeStyleDTO로 변환
            RecipeStyleDTO recipeStyleDTO = convertJsonStyleVOToRecipeStyleDTO(jsonStyleVO);

            // 6. iconId 추출하여 JSON 문자열로 변환
            Map<String, Object> iconInfoMap = extractIconIdMap(jsonStyleVO);
            String iconInfoJson = null;
            if (!iconInfoMap.isEmpty()) {
                iconInfoJson = objectMapper.writeValueAsString(iconInfoMap);
                log.debug("iconId Map 저장: {}", iconInfoJson);
            }

            // 7. DB 업데이트
            RecipeLayerStyleVO serverStyleVO = RecipeLayerStyleVO.builder()
                    .styleId(styleId)
                    .wrtrId(userId)
                    .styleNm(recipeStyleDTO.getStyleNm())
                    .styleSrvcNm(styleSrvcNm) // 기존 styleSrvcNm 유지
                    .styleTypeCd(recipeStyleDTO.getStyleTypeCd())
                    .spceTy(recipeStyleDTO.getSpceTy())
                    .iconInfo(iconInfoJson) // 사용자 이미지 정보
                    .lastMdfcnDt(LocalDateTime.now())
                    .lastMdfcnId(userId)
                    .build();

            int result = recipeLayerStyleMapper.updateServerStyle(serverStyleVO, userId);

            if (result > 0) {
                log.info("DB 스타일 수정 성공 - styleId: {}, styleSrvcNm: {}", styleId, styleSrvcNm);

                ResultVO resultVO = new ResultVO();
                resultVO.setResultCode(0);
                resultVO.setResultMessage("스타일이 성공적으로 수정되었습니다.");
                resultVO.putResult("data", serverStyleVO);
                resultVO.putResult("styleSrvcNm", styleSrvcNm);
                resultVO.putResult("styleId", styleId.toString());
                return resultVO;
            } else {
                throw new Exception("DB 수정 실패");
            }

        } catch (Exception e) {
            log.error("레이어 스타일 수정 실패: {}", e.getMessage(), e);
            ResultVO resultVO = new ResultVO();
            resultVO.setResultCode(-1);
            log.debug("레이어 스타일 수정 - styleId: {}, layerId: {}, userId: {}", styleId, jsonStyleVO.getLayerId(), userId);
            resultVO.setResultMessage(String.format(
                    "레이어 스타일 수정 실패 - styleId: %s, layerId: %s, userId: %s",
                    styleId,
                    jsonStyleVO.getLayerId(),
                    userId));
            return resultVO;
        }
    }

    /**
     * 서버 스타일 단건 삭제
     *
     * 처리 순서:
     * 1. styleId null 체크
     * 2. DB에서 기존 스타일 정보 조회 (존재 여부 확인)
     * 3. styleSrvcNm이 있으면 MapPrime 서버에 DELETE 요청
     * 4. MapPrime 삭제 완료 후 DB에서 삭제
     *
     * 참고:
     * - styleSrvcNm이 없는 경우 MapPrime 삭제는 건너뛰고 DB만 삭제
     * - MapPrime 삭제 실패 시에도 DB 삭제는 진행 (경고 로그만 출력)
     *
     * @param styleId 삭제할 스타일 ID
     * @return ResultVO 삭제 결과 (성공: code=0, 실패: code=-1)
     * @throws Exception 스타일 없음, DB 삭제 실패 등
     */
    @Override
    @Transactional
    public ResultVO deleteServerStyle(Long styleId, String userId) throws Exception {
        log.debug("서버 스타일 삭제 - styleId: {}, userId: {}", styleId, userId);

        try {
            // 1. styleId null 체크
            if (styleId == null) {
                ResultVO result = new ResultVO();
                result.setResultCode(-1);
                result.setResultMessage("스타일 ID는 필수입니다.");
                return result;
            }

            // 2. 기존 스타일 존재 확인
            RecipeLayerStyleVO existingStyle = recipeLayerStyleMapper.selectServerStyle(styleId);
            if (existingStyle == null) {
                ResultVO result = new ResultVO();
                result.setResultCode(-1);
                result.setResultMessage("삭제할 스타일을 찾을 수 없습니다.");
                return result;
            }

            // [추가] 지정 스타일('D' 타입) 삭제 차단
            if ("D".equalsIgnoreCase(existingStyle.getStyleTypeCd())) {
                log.warn("지정 스타일 삭제 시도 차단 - styleId: {}, userId: {}", styleId, userId);
                ResultVO result = new ResultVO();
                result.setResultCode(-1);
                result.setResultMessage("지정 스타일(공용)은 삭제할 수 없습니다.");
                return result;
            }

            String styleSrvcNm = existingStyle.getStyleSrvcNm();
            String spceTy = existingStyle.getSpceTy();

            // 3. recipe_lyr 테이블 업데이트: 삭제할 스타일을 참조하는 레이어를 기본 스타일로 변경
            if (spceTy != null && !spceTy.trim().isEmpty()) {
                String spceTyLower = spceTy.toLowerCase();
                String fallbackStyleSrvcNm = null;

                // 타입에 맞는 기본 스타일명 생성 (랜덤 1~9)
                int randomNum = new Random().nextInt(9) + 1;

                if ("polygon".equalsIgnoreCase(spceTyLower)) {
                    fallbackStyleSrvcNm = "polygon_style_" + randomNum;
                } else if ("line".equalsIgnoreCase(spceTyLower) || "linestring".equalsIgnoreCase(spceTyLower)) {
                    fallbackStyleSrvcNm = "line_style_" + randomNum;
                } else if ("point".equalsIgnoreCase(spceTyLower)) {
                    fallbackStyleSrvcNm = "point_style_" + randomNum;
                }

                if (fallbackStyleSrvcNm != null) {
                    // DB에서 기본 스타일 존재 여부 확인 및 ID 가져오기
                    RecipeLayerStyleVO fallbackStyle = recipeLayerStyleMapper
                            .selectServerStyleByServiceName(fallbackStyleSrvcNm);

                    if (fallbackStyle != null && fallbackStyle.getStyleId() != null) {
                        // recipe_lyr 테이블 업데이트: 삭제할 styleId → 기본 스타일 ID
                        int updatedCount = recipeLayerMapper.updateRecipeLayerStyleId(styleId,
                                fallbackStyle.getStyleId());
                        log.info("recipe_lyr 업데이트 성공 - styleId: {} → fallbackStyleId: {} ({}), 업데이트 레코드 수: {}",
                                styleId, fallbackStyle.getStyleId(), fallbackStyleSrvcNm, updatedCount);
                    } else {
                        // 기본 스타일이 DB에 없는 경우
                        log.error("기본 스타일이 DB에 존재하지 않습니다 - fallbackStyleSrvcNm: {}", fallbackStyleSrvcNm);
                        ResultVO result = new ResultVO();
                        result.setResultCode(-1);
                        result.setResultMessage("기본 스타일(" + fallbackStyleSrvcNm + ")이 DB에 없습니다. 스타일 삭제를 중단합니다.");
                        return result;
                    }
                }
            } else {
                log.warn("스타일 타입(spceTy)이 없습니다 - styleId: {}, styleSrvcNm: {}", styleId, styleSrvcNm);
            }

            // 4. MapPrime 서버에서 스타일 삭제 (styleSrvcNm이 있는 경우만)
            if (styleSrvcNm != null && !styleSrvcNm.isEmpty()) {
                log.debug("MapPrime 서버 스타일 삭제 시작 - styleSrvcNm: {}", styleSrvcNm);
                String deleteUrl = mapprimeServerUrl + "/rest/style/" + styleSrvcNm;

                ResponseEntity<String> response = restTemplate.exchange(deleteUrl, HttpMethod.DELETE, null,
                        String.class);

                if (response.getStatusCode().is2xxSuccessful()) {
                    log.info("MapPrime 서버 스타일 삭제 성공 - styleSrvcNm: {}", styleSrvcNm);
                } else {
                    log.warn("MapPrime 서버 스타일 삭제 실패 - styleSrvcNm: {}, status: {}",
                            styleSrvcNm, response.getStatusCode());
                }
            } else {
                log.debug("styleSrvcNm이 없어 MapPrime 서버 삭제 건너뜀");
            }

            // 5. DB에서 스타일 삭제 (userId로 본인 것만 삭제)
            int result = recipeLayerStyleMapper.deleteServerStyle(styleId, userId);

            if (result > 0) {
                log.info("DB 스타일 삭제 성공 - styleId: {}, styleName: {}, styleSrvcNm: {}",
                        styleId, existingStyle.getStyleNm(), styleSrvcNm);
                ResultVO resultVO = new ResultVO();
                resultVO.setResultCode(0);
                resultVO.setResultMessage("스타일이 성공적으로 삭제되었습니다.");
                return resultVO;
            } else {
                throw new Exception("DB 삭제 실패");
            }

        } catch (Exception e) {
            log.debug("서버 스타일 삭제 - styleId: {}, userId: {}", styleId, userId);
            log.error("서버 스타일 삭제 실패: {}", e.getMessage(), e);
            ResultVO resultVO = new ResultVO();
            resultVO.setResultCode(-1);
            resultVO.setResultMessage(String.format(
                    "서버 스타일 삭제 실패 - styleId: %s, userId: %s",
                    styleId,
                    userId));
            return resultVO;
        }
    }

    /**
     * 서버 스타일 다중 삭제
     *
     * 처리 순서:
     * 1. styleIds null/empty 체크
     * 2. 각 스타일마다 DB에서 조회 후 styleSrvcNm 확인
     * 3. styleSrvcNm이 있으면 MapPrime 서버에 DELETE 요청 (실패 시 계속 진행)
     * 4. 모든 MapPrime 삭제 시도 후 DB에서 일괄 삭제
     *
     * 참고:
     * - MapPrime 삭제는 개별적으로 시도하며, 실패하더라도 다음 스타일 계속 처리
     * - DB 삭제는 Mapper의 deleteServerStyles로 한 번에 처리
     * - MapPrime 삭제 성공 개수와 DB 삭제 개수를 함께 반환
     *
     * @param styleIds 삭제할 스타일 ID 목록
     * @return ResultVO 삭제 결과 (성공: code=0, 실패: code=-1)
     * @throws Exception styleIds 없음, DB 삭제 실패 등
     */
    @Override
    @Transactional
    public ResultVO deleteServerStyles(List<Long> styleIds, String userId) throws Exception {
        log.debug("서버 스타일 다중 삭제 - styleIds: {}, userId: {}", styleIds, userId);

        try {
            // 1. styleIds null/empty 체크
            if (styleIds == null || styleIds.isEmpty()) {
                ResultVO result = new ResultVO();
                result.setResultCode(-1);
                result.setResultMessage("삭제할 스타일 ID 목록이 필요합니다.");
                return result;
            }

            int mapprimeDeletedCount = 0;

            // 2~3. 각 스타일별로 recipe_lyr 업데이트 후 MapPrime 서버에서 삭제
            for (Long styleId : styleIds) {
                RecipeLayerStyleVO existingStyle = recipeLayerStyleMapper.selectServerStyle(styleId);
                if (existingStyle != null) {
                    // [추가] 다중 삭제 시 지정 스타일 포함 여부 체크
                    if ("D".equalsIgnoreCase(existingStyle.getStyleTypeCd())) {
                        log.warn("다중 삭제 중 지정 스타일 포함 발견 - styleId: {}", styleId);
                        ResultVO result = new ResultVO();
                        result.setResultCode(-1);
                        result.setResultMessage(
                                "삭제 목록에 지정 스타일(공용)이 포함되어 있어 삭제할 수 없습니다. (" + existingStyle.getStyleNm() + ")");
                        return result;
                    }

                    String styleSrvcNm = existingStyle.getStyleSrvcNm();
                    String spceTy = existingStyle.getSpceTy();

                    // spceTy가 있는 경우 타입별 대체 스타일 찾기
                    if (spceTy != null && !spceTy.trim().isEmpty()) {
                        String spceTyLower = spceTy.toLowerCase();
                        String fallbackStyleSrvcNm = null;

                        // 타입에 맞는 기본 스타일명 생성 (랜덤 1~9)
                        int randomNum = new Random().nextInt(9) + 1;

                        if ("polygon".equalsIgnoreCase(spceTyLower)) {
                            fallbackStyleSrvcNm = "polygon_style_" + randomNum;
                        } else if ("line".equalsIgnoreCase(spceTyLower) || "linestring".equalsIgnoreCase(spceTyLower)) {
                            fallbackStyleSrvcNm = "line_style_" + randomNum;
                        } else if ("point".equalsIgnoreCase(spceTyLower)) {
                            fallbackStyleSrvcNm = "point_style_" + randomNum;
                        }

                        if (fallbackStyleSrvcNm != null) {
                            // DB에서 기본 스타일 존재 여부 확인 및 ID 가져오기
                            RecipeLayerStyleVO fallbackStyle = recipeLayerStyleMapper
                                    .selectServerStyleByServiceName(fallbackStyleSrvcNm);

                            if (fallbackStyle != null && fallbackStyle.getStyleId() != null) {
                                // recipe_lyr 테이블 업데이트: 삭제할 styleId → 기본 스타일 ID
                                int updatedCount = recipeLayerMapper.updateRecipeLayerStyleId(styleId,
                                        fallbackStyle.getStyleId());
                                log.info("recipe_lyr 업데이트 성공 - styleId: {} → fallbackStyleId: {} ({}), 업데이트 레코드 수: {}",
                                        styleId, fallbackStyle.getStyleId(), fallbackStyleSrvcNm, updatedCount);
                            } else {
                                // 기본 스타일이 DB에 없는 경우
                                log.error("기본 스타일이 DB에 존재하지 않습니다 - fallbackStyleSrvcNm: {}", fallbackStyleSrvcNm);
                                throw new Exception("기본 스타일(" + fallbackStyleSrvcNm + ")이 DB에 없습니다. 스타일 삭제를 중단합니다.");
                            }
                        }
                    } else {
                        // spceTy가 null인 경우: 참조하는 recipe_lyr이 있는지 확인
                        log.warn("스타일 타입(spceTy)이 없습니다 - styleId: {}, styleSrvcNm: {}", styleId, styleSrvcNm);
                        // 타입을 알 수 없으므로 스타일 삭제 시도 (외래 키 위반 발생 가능)
                    }

                    // MapPrime 서버에서 삭제 (styleSrvcNm이 있는 경우만)
                    if (styleSrvcNm != null && !styleSrvcNm.isEmpty()) {
                        try {
                            String deleteUrl = mapprimeServerUrl + "/rest/style/" + styleSrvcNm;
                            ResponseEntity<String> response = restTemplate.exchange(deleteUrl, HttpMethod.DELETE, null,
                                    String.class);

                            if (response.getStatusCode().is2xxSuccessful()) {
                                mapprimeDeletedCount++;
                                log.debug("MapPrime 서버 스타일 삭제 성공 - styleSrvcNm: {}", styleSrvcNm);
                            } else {
                                log.warn("MapPrime 서버 스타일 삭제 실패 - styleSrvcNm: {}, status: {}",
                                        styleSrvcNm, response.getStatusCode());
                            }
                        } catch (Exception e) {
                            log.warn("MapPrime 서버 스타일 삭제 중 오류 - styleSrvcNm: {}", styleSrvcNm, e);
                        }
                    }
                }
            }

            // 4. DB에서 일괄 삭제 (userId로 본인 것만 삭제)
            int result = recipeLayerStyleMapper.deleteServerStyles(styleIds, userId);

            if (result > 0) {
                log.info("DB 스타일 다중 삭제 성공 - DB 삭제: {}개, MapPrime 삭제: {}개", result, mapprimeDeletedCount);
                ResultVO resultVO = new ResultVO();
                resultVO.setResultCode(0);
                resultVO.setResultMessage(String.format("%d개의 스타일이 성공적으로 삭제되었습니다.", result));
                resultVO.putResult("deletedCount", result);
                resultVO.putResult("mapprimeDeletedCount", mapprimeDeletedCount);
                return resultVO;
            } else {
                throw new Exception("DB 삭제 실패");
            }

        } catch (Exception e) {
            log.error("서버 스타일 다중 삭제 실패: {}", e.getMessage(), e);
            ResultVO resultVO = new ResultVO();
            resultVO.setResultCode(-1);
            resultVO.setResultMessage(String.format(
                    "서버 스타일 다중 삭제 실패 - styleIds: %s, userId: %s",
                    styleIds,
                    userId));
            return resultVO;
        }
    }

    @Override
    public boolean checkStyleNameDuplicate(String styleName, String wrtrId) throws Exception {
        log.debug("스타일 이름 중복 확인 - styleName: {}, wrtrId: {}", styleName, wrtrId);

        if (!StringUtils.hasText(styleName) || !StringUtils.hasText(wrtrId)) {
            return false;
        }

        int count = recipeLayerStyleMapper.checkStyleNameDuplicate(styleName, wrtrId);
        return count > 0;
    }

    @Override
    public List<RecipeStyleDTO> selectStylesByType(String styleTypeCd, String wrtrId) throws Exception {
        log.debug("스타일 타입별 조회 - styleTypeCd: {}, wrtrId: {}", styleTypeCd, wrtrId);

        if (!StringUtils.hasText(styleTypeCd)) {
            throw new IllegalArgumentException("스타일 타입 코드는 필수입니다.");
        }

        List<RecipeLayerStyleVO> voList = recipeLayerStyleMapper.selectStylesByType(styleTypeCd, wrtrId);
        return convertToRecipeStyleDTOList(voList);
    }

    @Override
    public JsonStyleVO selectServerStyleByServiceName(String styleSrvcNm) throws Exception {
        log.debug("스타일 서비스명으로 조회 - styleSrvcNm: {}", styleSrvcNm);

        if (!StringUtils.hasText(styleSrvcNm)) {
            throw new IllegalArgumentException("스타일 서비스명은 필수입니다.");
        }

        // RecipeLayerStyleVO result =
        // recipeLayerStyleMapper.selectServerStyleByServiceName(styleSrvcNm);
        String url = mapprimeServerUrl + "/rest/sld/" + styleSrvcNm;
        ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
        if (!response.getStatusCode().is2xxSuccessful()) {
            throw new Exception("MapPrime 서버에서 스타일 조회 실패: " + response.getStatusCode());
        }

        String sldXml = response.getBody();
        JsonStyleVO jsonStyle = styleJsonConverter.convertToJson(sldXml, styleSrvcNm);

        if (jsonStyle == null) {
            throw new IllegalArgumentException("해당 스타일 서비스명을 찾을 수 없습니다: " + styleSrvcNm);
        }

        return jsonStyle;
    }

    /**
     * RecipeLayerStyleVO를 RecipeStyleDTO로 변환 (작성자/수정자 정보 제외)
     */
    private RecipeStyleDTO convertToRecipeStyleDTO(RecipeLayerStyleVO vo) {
        if (vo == null) {
            return null;
        }

        boolean isDesignated = "D".equalsIgnoreCase(vo.getStyleTypeCd());

        return RecipeStyleDTO.builder()
                .styleId(vo.getStyleId()) // D 타입도 styleId 반환 (프론트엔드 해제 기능에 필요)
                .styleNm(vo.getStyleNm())
                .styleSrvcNm(vo.getStyleSrvcNm())
                .styleTypeCd(vo.getStyleTypeCd())
                .spceTy(vo.getSpceTy())
                .iconInfo(vo.getIconInfo())
                .frstRegDt(vo.getFrstRegDt())
                .lastMdfcnDt(vo.getLastMdfcnDt())
                .readonly(isDesignated ? true : null) // 'D' 타입은 읽기 전용 플래그 설정
                .build();
    }

    /**
     * RecipeLayerStyleVO 목록을 RecipeStyleDTO 목록으로 변환
     */
    private List<RecipeStyleDTO> convertToRecipeStyleDTOList(List<RecipeLayerStyleVO> voList) {
        if (voList == null) {
            return null;
        }

        return voList.stream()
                .map(this::convertToRecipeStyleDTO)
                .collect(Collectors.toList());
    }

    @Override
    public String convertJsonToSld(JsonStyleVO jsonStyle) throws Exception {
        log.info("JSON을 SLD로 변환 시작 - 레이어: {}, 스타일 방식: {}",
                jsonStyle.getLayerName(), jsonStyle.getStyleMode());

        try {
            // 내보낸 시간 설정
            jsonStyle.setExportedAt(LocalDateTime.now());

            // StyleConverter를 사용하여 변환
            String sldContent = styleConverter.convertToSld(jsonStyle);

            log.debug("SLD 변환 완료 - 길이: {} 문자", sldContent.length());
            return sldContent;

        } catch (Exception e) {
            log.error("JSON을 SLD로 변환 실패: {}", e.getMessage(), e);
            throw new Exception("JSON을 SLD로 변환 실패: " + e.getMessage(), e);
        }
    }

    @Override
    public JsonStyleVO convertSldToJson(String sldContent, String layerName) throws Exception {
        return convertSldToJson(sldContent, layerName, null);
    }

    @Override
    public JsonStyleVO convertSldToJson(String sldContent, String layerName, java.util.Map<String, Object> iconInfoMap)
            throws Exception {
        log.info("SLD를 JSON으로 변환 시작 - 레이어: {}, icon_info 포함: {}", layerName, iconInfoMap != null);

        try {
            // StyleJsonConverter를 사용하여 변환 (icon_info 포함)
            JsonStyleVO jsonStyle = styleJsonConverter.convertToJson(sldContent, layerName, iconInfoMap);

            log.debug("JSON 변환 완료 - 레이어명: {}, 스타일 모드: {}",
                    jsonStyle.getLayerName(), jsonStyle.getStyleMode());
            return jsonStyle;

        } catch (Exception e) {
            log.error("SLD를 JSON으로 변환 실패: {}", e.getMessage(), e);
            throw new Exception("SLD를 JSON으로 변환 실패: " + e.getMessage(), e);
        }
    }

    @Override
    public Map<String, Object> validateSld(String sldContent) throws Exception {
        log.info("SLD 유효성 검증 시작");

        Map<String, Object> validationResult = new HashMap<>();

        try {
            if (!StringUtils.hasText(sldContent)) {
                validationResult.put("valid", false);
                validationResult.put("error", "SLD 내용이 비어있습니다");
                return validationResult;
            }

            // 기본 XML 형식 검증
            String trimmedSld = sldContent.trim();
            if (!trimmedSld.startsWith("<") || !trimmedSld.endsWith(">")) {
                validationResult.put("valid", false);
                validationResult.put("error", "올바른 XML 형식이 아닙니다");
                return validationResult;
            }

            // SLD 태그 존재 확인
            if (!trimmedSld.contains("<StyledLayerDescriptor") && !trimmedSld.contains("<sld:")) {
                validationResult.put("valid", false);
                validationResult.put("error", "SLD(Styled Layer Descriptor) 형식이 아닙니다");
                return validationResult;
            }

            validationResult.put("valid", true);
            validationResult.put("message", "SLD 유효성 검증 통과");

            log.info("SLD 유효성 검증 성공");
            return validationResult;

        } catch (Exception e) {
            log.error("SLD 유효성 검증 실패", e);
            validationResult.put("valid", false);
            validationResult.put("error", "SLD 유효성 검증 중 오류 발생");
            return validationResult;
        }
    }

    @Override
    public boolean existsMapprimeStyle(String styleSrvcNm) throws Exception {
        log.debug("MapPrime 스타일 존재 확인 - styleSrvcNm: {}", styleSrvcNm);

        if (!StringUtils.hasText(styleSrvcNm)) {
            return false;
        }

        try {
            // MapPrime 서버에서 스타일 조회
            String url = mapprimeServerUrl + "/rest/style/" + styleSrvcNm;
            restTemplate.getForObject(url, String.class);

            log.debug("MapPrime 스타일 존재함 - styleSrvcNm: {}", styleSrvcNm);
            return true;

        } catch (Exception e) {
            log.debug("MapPrime 스타일 존재하지 않음 - styleSrvcNm: {}", styleSrvcNm);
            return false;
        }
    }

    /**
     * 스타일 ID로 SLD XML 조회
     *
     * 처리 순서:
     * 1. DB에서 styleId로 스타일 정보 조회
     * 2. styleSrvcNm 존재 여부 확인
     * 3. MapPrime 서버에 GET 요청으로 SLD XML 조회
     *
     * @param styleId 스타일 ID
     * @return SLD XML 문자열
     * @throws Exception 스타일 없음, styleSrvcNm 없음, MapPrime 조회 실패 등
     */
    @Override
    public String getSldByStyleId(Long styleId) throws Exception {
        log.debug("스타일 ID로 SLD XML 조회 - styleId: {}", styleId);

        // 1. DB에서 스타일 정보 조회
        RecipeLayerStyleVO styleVO = recipeLayerStyleMapper.selectServerStyle(styleId);
        if (styleVO == null) {
            throw new IllegalArgumentException("스타일 ID " + styleId + "를 찾을 수 없습니다.");
        }

        // 2. styleSrvcNm 확인
        String styleSrvcNm = styleVO.getStyleSrvcNm();
        if (!StringUtils.hasText(styleSrvcNm)) {
            throw new IllegalArgumentException("MapPrime 스타일 서비스명이 없습니다.");
        }

        try {
            // 3. MapPrime 서버에서 SLD XML 조회
            String url = mapprimeServerUrl + "/rest/sld/" + styleSrvcNm;
            log.debug("MapPrime SLD 조회 요청 - URL: {}", url);

            ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);

            if (!response.getStatusCode().is2xxSuccessful()) {
                throw new Exception("MapPrime 서버에서 SLD 조회 실패: " + response.getStatusCode());
            }

            String sldXml = response.getBody();
            log.info("MapPrime SLD XML 조회 성공 - styleId: {}, styleSrvcNm: {}", styleId, styleSrvcNm);

            return sldXml;

        } catch (Exception e) {
            log.error("MapPrime SLD XML 조회 실패 - styleId: {}, styleSrvcNm: {}, 오류", styleId, styleSrvcNm, e);
            throw new Exception("MapPrime 서버에서 SLD 조회 실패: " + e.getMessage(), e);
        }
    }

    /**
     * 선택된 분류 방식에 따른 구간 경계값 리스트를 반환합니다.
     *
     * @param request 분류 요청 정보
     * @return 계산된 구간 경계값(Double) 리스트
     */
    @Override
    public List<Range> getClassification(ClassificationRequestDTO request) throws IOException {

        String lyrPhysNm;
        if (request.getLayerType().equals(LayerType.TASK)) {
            lyrPhysNm = recipeLayerStyleMapper.getLyrPhysNmByTaskLyr(request.getLayerId());
        } else {
            lyrPhysNm = recipeLayerStyleMapper.getLyrPhysNmByUserLyr(request.getLayerId());
        }

        if (lyrPhysNm == null || lyrPhysNm.isEmpty()) {
            throw new IllegalArgumentException(String.format("레이어 정보를 찾을 수 없습니다. layerId=%d, layerType=%s",
                    request.getLayerId(), request.getLayerType()));
        }

        switch (request.getMethod()) {
            case EQUAL_INTERVAL -> {
                return getEqualClassification(lyrPhysNm, request);
            }
            case QUANTILE -> {
                return getQuantileClassification(lyrPhysNm, request);
            }
            case NATURAL_BREAKS -> {
                Map<String, Object> params = new HashMap<>();
                params.put(PostgisNGDataStoreFactory.DBTYPE.key, "postgis");
                params.put(PostgisNGDataStoreFactory.DATASOURCE.key, dataSource);
                params.put(PostgisNGDataStoreFactory.SCHEMA.key, lyrPhysNm.split("\\.")[0]);
                PostgisNGDataStoreFactory factory = new PostgisNGDataStoreFactory();
                JDBCDataStore jdbcDataStore = factory.createDataStore(params);
                try {
                    SimpleFeatureSource featureSource = jdbcDataStore.getFeatureSource(lyrPhysNm.split("\\.")[1]);
                    SimpleFeatureCollection simpleFeatureCollection = featureSource
                            .getFeatures(new Query(lyrPhysNm.split("\\.")[1], Filter.INCLUDE));
                    List<Range> ranges = ClassificationUtils.getVectorClassification(
                            simpleFeatureCollection,
                            request.getColumnName(),
                            null,
                            request.getNumBreaks(),
                            request.getMethod(),
                            null,
                            null);
                    return ranges;
                } finally {
                    if (jdbcDataStore != null) {
                        jdbcDataStore.dispose();
                    }
                }
            }
            default -> {
                throw new IllegalArgumentException("지원 하지 않는 분류 방식: " + request.getMethod());
            }
        }
    }

    /**
     * 유형별 분류: 칼럼의 고유값 목록 조회 (문자열 파라미터)
     */
    @Override
    public List<CategoryValueDTO> getCategoryValues(String layerId, String layerType, String columnName, Integer limit,
            String orderBy) {
        if (layerId == null || layerId.isEmpty()) {
            throw new IllegalArgumentException("레이어 ID가 비어있습니다.");
        }

        if (columnName == null || columnName.isEmpty()) {
            throw new IllegalArgumentException("컬럼명이 비어있습니다.");
        }

        // 기본값/상한 설정
        if (limit == null || limit <= 0) {
            limit = MAX_CATEGORY_VALUES;
        }
        final int requestedLimit = Math.min(limit, MAX_CATEGORY_VALUES);
        final int queryLimit = requestedLimit == MAX_CATEGORY_VALUES ? MAX_CATEGORY_VALUES + 1 : requestedLimit;

        if (orderBy == null || (!orderBy.equalsIgnoreCase("ASC") && !orderBy.equalsIgnoreCase("DESC"))) {
            orderBy = "ASC";
        }

        // 1. layerId로 물리 테이블명 조회
        String lyrPhysNm;
        try {
            int id = Integer.parseInt(layerId);
            if ("TASK".equalsIgnoreCase(layerType)) {
                lyrPhysNm = recipeLayerStyleMapper.getLyrPhysNmByTaskLyr(id);
            } else {
                lyrPhysNm = recipeLayerStyleMapper.getLyrPhysNmByUserLyr(id);
            }
        } catch (NumberFormatException e) {
            // layerId가 이미 물리명인 경우 (rdl_cctv_p 같은 문자열)
            lyrPhysNm = layerId;
        }

        if (lyrPhysNm == null || lyrPhysNm.isEmpty()) {
            throw new IllegalArgumentException(
                    String.format("레이어 물리명을 찾을 수 없습니다. layerId=%s, layerType=%s", layerId, layerType));
        }

        // 2. Mapper 호출하여 고유값 조회 (limit+1 조회로 초과 여부 판단)
        List<CategoryValueDTO> categoryValues = recipeLayerStyleMapper.getCategoryValues(lyrPhysNm, columnName, queryLimit,
                orderBy);

        if (requestedLimit == MAX_CATEGORY_VALUES && categoryValues.size() > MAX_CATEGORY_VALUES) {
            return categoryValues.subList(0, MAX_CATEGORY_VALUES);
        }

        return categoryValues;
    }

    private List<Range> getEqualClassification(String lyrPhysNm, ClassificationRequestDTO request) {
        List<Range> ranges = new ArrayList<>();
        List<Double> classification = recipeLayerStyleMapper.getEqualClassification(lyrPhysNm, request.getColumnName(),
                request.getNumBreaks());
        int lastIndex = classification.size() - 2;
        for (int i = 0; i <= lastIndex; i++) {
            Double min = classification.get(i);
            Double max = classification.get(i + 1);
            boolean minIncluded = true;
            boolean maxIncluded = (i == lastIndex);
            ranges.add(new Range<>(min, minIncluded, max, maxIncluded));
        }
        return ranges;
    }

    private List<Range> getQuantileClassification(String lyrPhysNm, ClassificationRequestDTO request) {
        List<Range> ranges = new ArrayList<>();
        List<Double> classification = recipeLayerStyleMapper.getQuantileClassification(lyrPhysNm,
                request.getColumnName(), request.getNumBreaks());
        int lastIndex = classification.size() - 2;
        for (int i = 0; i <= lastIndex; i++) {
            Double min = classification.get(i);
            Double max = classification.get(i + 1);
            boolean minIncluded = true;
            boolean maxIncluded = (i == lastIndex);
            ranges.add(new Range<>(min, minIncluded, max, maxIncluded));
        }
        return ranges;
    }

    /**
     * JsonStyleVO에서 iconId를 추출하여 Map 형태로 반환
     * - Single: {"0": iconId}
     * - Graduated/Categorized: {"0": iconId1, "1": iconId2, ...}
     *
     * @param jsonStyleVO JSON 스타일 객체
     * @return iconId Map (인덱스 → iconId)
     */
    private Map<String, Object> extractIconIdMap(JsonStyleVO jsonStyleVO) {
        Map<String, Object> iconIdMap = new HashMap<>();

        if (jsonStyleVO == null) {
            return iconIdMap;
        }

        String styleMode = jsonStyleVO.getStyleMode();

        // Single 스타일 처리
        if ("single".equals(styleMode) && jsonStyleVO.getStyle() != null) {
            JsonStyleVO.SymbolConfig symbol = jsonStyleVO.getStyle().getSymbol();
            if (symbol != null && "userIcon".equals(symbol.getType()) && symbol.getIconId() != null) {
                iconIdMap.put("0", symbol.getIconId());
            }
        }
        // Graduated 스타일 처리
        else if ("graduated".equals(styleMode) && jsonStyleVO.getClassificationConfig() != null) {
            List<JsonStyleVO.RangeRule> ranges = jsonStyleVO.getClassificationConfig().getRanges();
            if (ranges != null) {
                for (int i = 0; i < ranges.size(); i++) {
                    JsonStyleVO.RangeRule range = ranges.get(i);
                    if (range.getStyle() != null && range.getStyle().getSymbol() != null) {
                        JsonStyleVO.SymbolConfig symbol = range.getStyle().getSymbol();
                        if ("userIcon".equals(symbol.getType()) && symbol.getIconId() != null) {
                            iconIdMap.put(String.valueOf(i), symbol.getIconId());
                        }
                    }
                }
            }
        }
        // Categorized 스타일 처리
        else if ("categorized".equals(styleMode) && jsonStyleVO.getClassificationConfig() != null) {
            List<JsonStyleVO.CategoryRule> categories = jsonStyleVO.getClassificationConfig().getCategories();
            if (categories != null) {
                for (int i = 0; i < categories.size(); i++) {
                    JsonStyleVO.CategoryRule category = categories.get(i);
                    if (category.getStyle() != null && category.getStyle().getSymbol() != null) {
                        JsonStyleVO.SymbolConfig symbol = category.getStyle().getSymbol();
                        if ("userIcon".equals(symbol.getType()) && symbol.getIconId() != null) {
                            iconIdMap.put(String.valueOf(i), symbol.getIconId());
                        }
                    }
                }
            }
        }

        return iconIdMap;
    }

    @Override
    public RecipeStyleDTO selectRandomStyle(String spceTy, int styleNumber) throws Exception {
        log.debug("랜덤 스타일 조회 - spceTy: {}, styleNumber: {}", spceTy, styleNumber);

        if (!StringUtils.hasText(spceTy)) {
            throw new IllegalArgumentException("공간 타입은 필수입니다.");
        }

        // 서비스명 생성 (예: polygon + _style_ + 1 -> polygon_style_1)
        String styleSrvcNm = spceTy.toLowerCase() + "_style_" + styleNumber;

        RecipeLayerStyleVO vo = recipeLayerStyleMapper.selectRandomStyle(styleSrvcNm);

        if (vo == null) {
            log.warn("조건에 맞는 랜덤 스타일을 찾을 수 없습니다. styleSrvcNm: {}", styleSrvcNm);
            return null;
        }

        return convertToRecipeStyleDTO(vo);
    }
}
