package incheon.ags.mrb.upload.service;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import org.geotools.geojson.geom.GeometryJSON;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import incheon.ags.mrb.main.exception.ErrorCode;
import incheon.ags.mrb.main.exception.RecipeException;
import incheon.ags.mrb.upload.service.util.CoordinateValidationUtil;
import incheon.ags.mrb.upload.vo.FileUploadRequestDTO;

@Service
public class ProcessDxfFileService {

    private static final Logger logger = LoggerFactory.getLogger(ProcessDxfFileService.class);
    
    private final TableCreationService tableCreationService;
    private final DataInsertionService dataInsertionService;
    private final FileProcessUtilService fileProcessUtilService;
    @Autowired private ObjectMapper objectMapper;

    public ProcessDxfFileService(
            TableCreationService tableCreationService,
            DataInsertionService dataInsertionService,
            FileProcessUtilService fileProcessUtilService) {
        this.tableCreationService = tableCreationService;
        this.dataInsertionService = dataInsertionService;
        this.fileProcessUtilService = fileProcessUtilService;
    }
    
    public Map<String, Object> processDxfFile(FileUploadRequestDTO dto) {
    	Path tempDir = null;
        Path sourceFilePath = null;
        Path tempOutputFilePath = null;
        String normalizedSelectedGeometryType = normalizeGeometryType(dto.getDxfGeometryType());
        
        if (normalizedSelectedGeometryType == null) {
            String normalizedSelection = normalizeGeometrySelection(dto.getDxfGeometrySelection());
            normalizedSelectedGeometryType = mapGeometryFamilyToType(normalizedSelection);
        }
        
        String forcedGeometryType = resolveForcedGeometryTypeForUpload(normalizedSelectedGeometryType);
        
        try {
            tempDir = fileProcessUtilService.createTempDirectory();
            sourceFilePath = fileProcessUtilService.saveMultipartFileToTemp(dto.getFile(), tempDir);
            
            if (!Files.exists(sourceFilePath) || Files.size(sourceFilePath) == 0) {
                throw new RecipeException(ErrorCode.FILE_EMPTY, "업로드된 DXF 파일을 찾을 수 없거나 비어 있습니다.");
            }
            
            String tempFileName = "dxf_output_" + UUID.randomUUID().toString() + ".geojson";
            tempOutputFilePath = tempDir.resolve(tempFileName);
            
            // 1. GDAL 변환 (DXF -> GeoJSON)
            ProcessBuilder processBuilder = dxfCommand(
                sourceFilePath.toString(), 
                tempOutputFilePath.toString(), 
                dto.getFilecoordinate(),
                dto.getDxfLayerName(),
                forcedGeometryType
            );
            
            Process process = processBuilder.start();
            String errorLog;
            try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
                errorLog = errReader.lines().collect(Collectors.joining("\n"));
            }
            
            if (errorLog != null && !errorLog.isBlank()) {
                logger.warn("GDAL conversion stderr: {}", errorLog); 
            }
            
            int exitCode = process.waitFor();
            
            if (exitCode != 0) {
                logger.error("GDAL conversion failed. ExitCode: {}, Error: {}", exitCode, errorLog);
                String rawMessage = "DXF 변환 실패 (exitCode=" + exitCode + ")";
                throw new RecipeException(ErrorCode.GDAL_CONVERSION_FAILED, 
                    rawMessage + (errorLog != null ? "\n" + errorLog : ""));
            }
            
            // 2. GeoJSON 파싱
            String geoJsonContent = Files.readString(tempOutputFilePath, StandardCharsets.UTF_8);
            JsonNode rootNode = objectMapper.readTree(geoJsonContent);
            JsonNode rawFeaturesArray = rootNode.get("features");
            
            if (rawFeaturesArray == null || rawFeaturesArray.size() == 0) {
                throw new RecipeException(ErrorCode.FILE_PARSE_FAILED, "DXF 파일에서 변환된 데이터가 없습니다.");
            }

            Map<String, Integer> geometryFamilyCounts = collectGeometryFamilies(rawFeaturesArray);
            
            // 3. 지오메트리 선택 검증
            validateGeometrySelection(dto.getDxfGeometrySelection(), geometryFamilyCounts);

            // 선택된 타입 결정
            Map<String, Object> selectionResult = resolveGeometrySelection(dto.getDxfGeometrySelection(), geometryFamilyCounts);
            String resolvedSelection = (String) selectionResult.get("resolvedSelection");

            ArrayNode filteredFeatures = filterFeaturesBySelection(
                    rawFeaturesArray,
                    objectMapper,
                    sourceFilePath,
                    tempDir,
                    dto.getFilecoordinate(),
                    dto.getDxfLayerName(),
                    resolvedSelection,
                    geometryFamilyCounts
            );

            if (filteredFeatures.isEmpty()) {
                throw new RecipeException(ErrorCode.INVALID_GEOMETRY_TYPE, "선택한 지오메트리 타입에 해당하는 DXF 피처가 없습니다.");
            }
            JsonNode featuresArray = filteredFeatures;
            
            // 4. 지오메트리 타입 불일치 검증
            String actualRawGeometryType = getGeometryType(featuresArray.get(0));
            String actualType = normalizeGeometryType(actualRawGeometryType);
            String selectedType = determineSelectedGeometryType(normalizedSelectedGeometryType, dto.getDxfGeometryType(), resolvedSelection);
            
            if (actualType != null && selectedType != null && !actualType.equals(selectedType)) {
                throw new RecipeException(ErrorCode.INVALID_GEOMETRY_TYPE, 
                    String.format("선택한 지오메트리 타입(%s)과 실제 DXF 파일의 타입(%s)이 일치하지 않습니다.", selectedType, actualType));
            }

            String postGISTableName = fileProcessUtilService.generateUniqueTableName();
            String columnsSql = fileProcessUtilService.generateColumnsFromFirstFeature(featuresArray.get(0));
            int srid = fileProcessUtilService.extractSridFromCoordinate(dto.getFilecoordinate());
            String geomType = resolveTableGeometryType(actualRawGeometryType, forcedGeometryType);
            
            // 5. 테이블 생성 및 데이터 삽입
            tableCreationService.createDxfTable(postGISTableName, geomType, srid, columnsSql);
            Map<String, Object> insertResult = dataInsertionService.insertDxfData(postGISTableName, featuresArray, dto);
            
            // 6. 결과 반환 (성공 시에만)
            Map<String, Object> result = new HashMap<>();
            result.put("insertedCount", insertResult.get("insertedCount"));
            result.put("safeTableName", postGISTableName);
            result.put("srid", srid);
            return result;
            
        } catch (IOException | InterruptedException e) {
            logger.error("DXF 파일 처리 중 오류가 발생했습니다.", e);
            throw new RecipeException(ErrorCode.FILE_PARSE_FAILED, "DXF 파일 처리 중 시스템 오류가 발생했습니다: " + e.getMessage());
        } finally {
            deleteTempFile(sourceFilePath);
            deleteTempFile(tempOutputFilePath);
            if (tempDir != null) {
                fileProcessUtilService.cleanupTempDirectory(tempDir);
            }
        }
    }
    
    private List<Map<String, Object>> inspectLayers(Path sourceFilePath) throws IOException, InterruptedException {
        if (!Files.exists(sourceFilePath)) {
            throw new RecipeException(ErrorCode.FILE_EMPTY, "DXF 파일을 찾을 수 없습니다.");
        }
        
        ProcessBuilder processBuilder = new ProcessBuilder("ogrinfo", "-so", "-json", sourceFilePath.toString());
        Process process = processBuilder.start();

        String jsonOutput;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
            jsonOutput = reader.lines().collect(Collectors.joining("\n"));
        }
        
        String errorOutput;
        try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
            errorOutput = errorReader.lines().collect(Collectors.joining("\n"));
        }
        
        int exitCode = process.waitFor();
        
        if (exitCode != 0) {
            logger.error("GDAL Process Failed. Exit Code: {}, Error: {}", exitCode, errorOutput);
            throw new RecipeException(ErrorCode.GDAL_CONVERSION_FAILED, "DXF 레이어 정보를 읽는 데 실패했습니다.");
        }

        JsonNode rootNode = objectMapper.readTree(jsonOutput);
        JsonNode layersNode = rootNode.path("layers");

        List<Map<String, Object>> layers = new ArrayList<>();
        if (layersNode.isArray()) {
            for (JsonNode layerNode : layersNode) {
                Map<String, Object> layerInfo = new HashMap<>();
                String name = layerNode.path("name").asText();
                String geometryType = layerNode.path("geometryType").asText();

                layerInfo.put("name", name);
                layerInfo.put("geometryType", geometryType);
                layerInfo.put("geometryFamily", resolveGeometryFamily(geometryType));
                layerInfo.put("featureCount", layerNode.path("featureCount").asInt(0));
                layerInfo.put("fields", extractFields(layerNode.path("fields")));

                layers.add(layerInfo);
            }
        }
        return layers;
    }

    private List<Map<String, Object>> extractFields(JsonNode fieldsNode) {
        List<Map<String, Object>> fields = new ArrayList<>();
        if (fieldsNode == null || !fieldsNode.isArray()) {
            return fields;
        }
        for (JsonNode fieldNode : fieldsNode) {
            Map<String, Object> fieldInfo = new LinkedHashMap<>();
            fieldInfo.put("name", fieldNode.path("name").asText(""));
            fieldInfo.put("type", fieldNode.path("type").asText(""));
            if (fieldNode.has("width")) {
                fieldInfo.put("width", fieldNode.path("width").asInt());
            }
            if (fieldNode.has("precision")) {
                fieldInfo.put("precision", fieldNode.path("precision").asInt());
            }
            fields.add(fieldInfo);
        }
        return fields;
    }

    public Map<String, Object> previewLayer(MultipartFile file, String layerName, String coordinate, int limit) throws IOException, InterruptedException {
        Path tempDir = null;
        try {
            tempDir = fileProcessUtilService.createTempDirectory();
            Path sourceFilePath = fileProcessUtilService.saveMultipartFileToTemp(file, tempDir);
            
            return executePreview(tempDir, sourceFilePath, layerName, coordinate, limit);
            
        } finally {
            if (tempDir != null) {
                fileProcessUtilService.cleanupTempDirectory(tempDir);
            }
        }
    }

    // ========================================================================
    // 2. DXF 분석 (레이어 목록 조회)
    // ========================================================================
    public Map<String, Object> analyzeDxfFile(MultipartFile file, String coordinate, int limit) throws IOException, InterruptedException {
        Path tempDir = null;
        try {
            tempDir = fileProcessUtilService.createTempDirectory();
            Path sourceFilePath = fileProcessUtilService.saveMultipartFileToTemp(file, tempDir);
            
            logger.info("Starting DXF analysis. File saved at: {}", sourceFilePath);

            List<Map<String, Object>> layerMetadata = inspectLayers(sourceFilePath);
            
            logger.info("inspectLayers completed. Found {} layers.", layerMetadata != null ? layerMetadata.size() : 0);

            List<Map<String, Object>> layers = new ArrayList<>();

            for (Map<String, Object> layerInfo : layerMetadata) {
                Map<String, Object> enrichedLayer = new LinkedHashMap<>(layerInfo);
                String layerName = String.valueOf(layerInfo.getOrDefault("name", ""));

                Map<String, Object> previewResult = new HashMap<>();
                if (layerName == null || layerName.isBlank()) {
                    logger.warn("Skipping preview for layer with empty name.");
                    previewResult.put("success", false);
                    previewResult.put("message", "레이어 이름을 확인할 수 없습니다.");
                } else {
                    try {
                        previewResult = executePreview(tempDir, sourceFilePath, layerName, null, limit);
                    } catch (RecipeException e) {
                         logger.warn("Preview generation failed for layer: {}. Reason: {}", layerName, e.getMessage());
                         previewResult.put("success", false);
                         previewResult.put("message", e.getMessage());
                    }
                }
                enrichedLayer.put("preview", previewResult);
                layers.add(enrichedLayer);
            }

            if (layers.isEmpty()) {
                 throw new RecipeException(ErrorCode.FILE_PARSE_FAILED, "DXF 파일에서 유효한 레이어를 찾을 수 없습니다.");
            }

            Map<String, Object> response = new HashMap<>();
            response.put("layers", layers);
            response.put("limit", limit);
            response.put("success", true);
            return response;

        } finally {
            if (tempDir != null) {
                fileProcessUtilService.cleanupTempDirectory(tempDir);
            }
        }
    }

    private Map<String, Object> executePreview(Path tempDir, Path sourceFilePath, String layerName, String coordinate, int limit) throws IOException, InterruptedException {
        Path tempOutputFilePath = tempDir.resolve("dxf_preview_" + UUID.randomUUID() + ".geojson");
        Path polygonTempOutputPath = tempDir.resolve("dxf_preview_polygon_" + UUID.randomUUID() + ".geojson");

        boolean coordinateProvided = coordinate != null && !coordinate.isBlank();
        String effectiveCoordinate = coordinateProvided ? coordinate.trim() : "";
        
        // 1. GDAL Preview Command 실행
        ProcessBuilder command = buildPreviewCommand(
            sourceFilePath.toString(),
            tempOutputFilePath.toString(),
            effectiveCoordinate,
            layerName,
            limit,
            null
        );
        
        Process process = command.start();
        String errorLog;
        try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
            errorLog = errReader.lines().collect(Collectors.joining("\n"));
        }
        
        int exitCode = process.waitFor();
        if (exitCode != 0) {
            String rawMessage = "DXF 미리보기 변환에 실패했습니다. (Exit code: " + exitCode + ")";
            if (errorLog != null && !errorLog.isBlank()) {
                rawMessage += "\n" + errorLog;
            }
            
            // 좌표계 오류인 경우 구체적 에러 (INVALID_COORDINATE)
            if (containsCoordinateProjectionError(errorLog)) {
                throw new RecipeException(ErrorCode.INVALID_COORDINATE, 
                    "선택한 좌표계와 DXF 데이터의 좌표가 일치하지 않습니다.\n데이터가 사용하는 EPSG 코드를 확인해주세요.");
            }
            // 그 외 변환 실패 (GDAL_CONVERSION_FAILED)
            throw new RecipeException(ErrorCode.GDAL_CONVERSION_FAILED, rawMessage);
        }

        String geoJsonContent = Files.readString(tempOutputFilePath, StandardCharsets.UTF_8);
        JsonNode rootNode = objectMapper.readTree(geoJsonContent);
        JsonNode featuresArray = rootNode.path("features");

        if (!featuresArray.isArray()) {
             throw new RecipeException(ErrorCode.FILE_PARSE_FAILED, "DXF 레이어에서 미리보기용 데이터를 찾을 수 없습니다.");
        }

        // 2. 데이터 파싱 및 지오메트리 분류
        Envelope envelope = new Envelope();
        GeometryJSON geometryJSON = new GeometryJSON();
        List<Map<String, Object>> rows = new ArrayList<>();
        Map<String, List<Map<String, Object>>> rowsByGeometry = new LinkedHashMap<>();
        Map<String, ArrayNode> featuresByGeometry = new LinkedHashMap<>();
        Map<String, Integer> geometryCounts = new LinkedHashMap<>();
        String firstGeometryType = null;

        for (JsonNode featureNode : featuresArray) {
            JsonNode geometryNode = featureNode.get("geometry");
            if (geometryNode != null && !geometryNode.isNull()) {
                try (StringReader geoReader = new StringReader(geometryNode.toString())) {
                    Geometry geometry = geometryJSON.read(geoReader);
                    if (geometry != null && !geometry.isEmpty()) {
                        Envelope geomEnv = geometry.getEnvelopeInternal();
                        if (!geomEnv.isNull()) {
                            envelope.expandToInclude(geomEnv);
                        }
                    }
                }
            }

            String geometryType = normalizeGeometryType(getGeometryType(featureNode));
            if (firstGeometryType == null) {
                firstGeometryType = geometryType;
            }

            // 속성 처리
            JsonNode propertiesNode = featureNode.path("properties");
            Map<String, Object> row = new HashMap<>();
            if (propertiesNode.isObject()) {
                propertiesNode.fields().forEachRemaining(entry -> {
                    JsonNode valueNode = entry.getValue();
                    row.put(entry.getKey(), valueNode.isNull() ? "" : valueNode.asText(""));
                });
            }
            rows.add(row);

            String geometryFamily = resolveGeometryFamily(geometryType);
            rowsByGeometry.computeIfAbsent(geometryFamily, key -> new ArrayList<>()).add(row);
            featuresByGeometry.computeIfAbsent(geometryFamily, key -> objectMapper.createArrayNode()).add(featureNode.deepCopy());
            geometryCounts.merge(geometryFamily, 1, Integer::sum);
        }
        
        // 3. 폴리곤 강제 변환 시도 (선택적)
        try {
             attemptPolygonForcedConversion(
                 sourceFilePath, polygonTempOutputPath, effectiveCoordinate, layerName, limit, 
                 objectMapper, geometryJSON, envelope, rowsByGeometry, featuresByGeometry, geometryCounts
             );
        } finally {
            deleteTempFile(polygonTempOutputPath);
        }

        // 4. 좌표계 및 범위 검증 (예외 발생 처리)
        if (coordinateProvided) {
            int srid = fileProcessUtilService.extractSridFromCoordinate(effectiveCoordinate);
            if (srid > 0) {
                Envelope originalEnvelope = extractOriginalEnvelope(tempDir, sourceFilePath, layerName, limit);
                if (originalEnvelope != null && !CoordinateValidationUtil.isEnvelopeConsistentWithSrid(originalEnvelope, srid)) {
                     throw new RecipeException(ErrorCode.INVALID_COORDINATE, 
                         String.format("선택한 좌표계(EPSG:%d)와 데이터 좌표 범위가 일치하지 않습니다.", srid));
                }
            }

            if (!envelope.isNull() && (!CoordinateValidationUtil.isWithinKorea(envelope)
                    || !CoordinateValidationUtil.isReasonableEnvelope(envelope))) {
                 throw new RecipeException(ErrorCode.OUT_OF_KOREA_BOUNDS, 
                     "선택한 좌표가 대한민국 지도 서비스 영역을 벗어났습니다. 올바른 좌표계를 선택했는지 확인해주세요.");
            }
        }

        if (rows.isEmpty()) {
             throw new RecipeException(ErrorCode.FILE_PARSE_FAILED, 
                 "미리보기 데이터를 해석할 수 없습니다. 좌표계 또는 DXF 데이터를 확인해주세요.");
        }

        // 5. 결과 구성 (Helper 메서드 사용)
        return buildPreviewResult(
            objectMapper, rootNode, rows, rowsByGeometry, featuresByGeometry, geometryCounts,
            firstGeometryType, effectiveCoordinate, limit
        );
    }

    // ========================================================================
    // [추가] Helper Methods for executePreview
    // ========================================================================

    // 폴리곤 강제 변환 로직 분리
    private void attemptPolygonForcedConversion(
            Path sourceFilePath, Path polygonTempOutputPath, String effectiveCoordinate, String layerName, int limit,
            ObjectMapper mapper, GeometryJSON geometryJSON, Envelope envelope,
            Map<String, List<Map<String, Object>>> rowsByGeometry, Map<String, ArrayNode> featuresByGeometry, Map<String, Integer> geometryCounts
    ) {
         try {
            ProcessBuilder polygonCommand = buildPreviewCommand(
                    sourceFilePath.toString(),
                    polygonTempOutputPath.toString(),
                    effectiveCoordinate,
                    layerName,
                    limit,
                    "POLYGON"
            );
            Process polygonProcess = polygonCommand.start();
            if (polygonProcess.waitFor() == 0) {
                String polygonGeoJsonContent = Files.readString(polygonTempOutputPath, StandardCharsets.UTF_8);
                JsonNode polygonRootNode = mapper.readTree(polygonGeoJsonContent);
                JsonNode polygonFeaturesArray = polygonRootNode.path("features");

                if (polygonFeaturesArray.isArray() && polygonFeaturesArray.size() > 0) {
                    for (JsonNode polygonFeatureNode : polygonFeaturesArray) {
                        JsonNode geometryNode = polygonFeatureNode.get("geometry");
                        if (geometryNode != null && !geometryNode.isNull()) {
                             try (StringReader geoReader = new StringReader(geometryNode.toString())) {
                                Geometry geometry = geometryJSON.read(geoReader);
                                if (geometry != null && !geometry.isEmpty() && !geometry.getEnvelopeInternal().isNull()) {
                                    envelope.expandToInclude(geometry.getEnvelopeInternal());
                                }
                            }
                        }
                        // 속성 처리
                        Map<String, Object> row = new HashMap<>();
                        JsonNode propertiesNode = polygonFeatureNode.path("properties");
                        if (propertiesNode.isObject()) {
                            propertiesNode.fields().forEachRemaining(entry -> {
                                JsonNode val = entry.getValue();
                                row.put(entry.getKey(), val.isNull() ? "" : val.asText(""));
                            });
                        }
                        String family = "POLYGON";
                        rowsByGeometry.computeIfAbsent(family, k -> new ArrayList<>()).add(row);
                        featuresByGeometry.computeIfAbsent(family, k -> mapper.createArrayNode()).add(polygonFeatureNode.deepCopy());
                        geometryCounts.merge(family, 1, Integer::sum);
                    }
                }
            }
        } catch (Exception e) {
            logger.debug("폴리곤 강제 변환 실패 (무시됨): {}", e.getMessage());
        }
    }

    // 미리보기 결과 구성 로직 분리
    private Map<String, Object> buildPreviewResult(
            ObjectMapper mapper, JsonNode rootNode, List<Map<String, Object>> rows,
            Map<String, List<Map<String, Object>>> rowsByGeometry, Map<String, ArrayNode> featuresByGeometry,
            Map<String, Integer> geometryCounts, String firstGeometryType, String effectiveCoordinate, int limit
    ) {
         // 1. Limit 적용
         Map<String, List<Map<String, Object>>> limitedRowsByGeometry = new LinkedHashMap<>();
         Map<String, ArrayNode> limitedGeojsonByGeometry = new LinkedHashMap<>();
         
         for (Map.Entry<String, List<Map<String, Object>>> entry : rowsByGeometry.entrySet()) {
             String family = entry.getKey();
             List<Map<String, Object>> familyRows = entry.getValue();
             if (familyRows == null || familyRows.isEmpty()) continue;
             
             List<Map<String, Object>> limitedRows = familyRows.stream().limit(limit).collect(Collectors.toList());
             limitedRowsByGeometry.put(family, limitedRows);
             
             ArrayNode featuresNode = featuresByGeometry.get(family);
             if (featuresNode != null) {
                 ArrayNode limitedFeatures = mapper.createArrayNode();
                 for(int i=0; i<Math.min(limit, featuresNode.size()); i++) {
                     limitedFeatures.add(featuresNode.get(i));
                 }
                 limitedGeojsonByGeometry.put(family, limitedFeatures);
             }
         }
         
         // 2. 전체(ALL) 데이터 구성
         List<Map<String, Object>> combinedRows = limitedRowsByGeometry.values().stream().flatMap(List::stream).collect(Collectors.toList());
         List<Map<String, Object>> limitedAllRows = combinedRows.stream().limit(limit).collect(Collectors.toList());
         ArrayNode limitedAllFeatures = mapper.createArrayNode();
         
         // Features 합치기
         outer:
         for (ArrayNode arrayNode : limitedGeojsonByGeometry.values()) {
             if (arrayNode == null) continue;
             for (JsonNode feature : arrayNode) {
                 limitedAllFeatures.add(feature);
                 if (limitedAllFeatures.size() >= limit) break outer;
             }
         }
         
         // 3. 최종 Map 구성
         Map<String, Object> result = new HashMap<>();
         result.put("success", true);
         result.put("rows", limitedAllRows);
         result.put("geojson", mapper.convertValue(rootNode, Object.class)); // 원본 전체 GeoJSON
         
         Map<String, Object> limitedGeojsonCollections = new LinkedHashMap<>();
         
         // ALL 타입 추가
         ObjectNode allCollection = mapper.createObjectNode();
         allCollection.put("type", "FeatureCollection");
         allCollection.set("features", limitedAllFeatures);
         limitedGeojsonCollections.put("ALL", mapper.convertValue(allCollection, Object.class));
         
         for (Map.Entry<String, ArrayNode> entry : limitedGeojsonByGeometry.entrySet()) {
              ObjectNode featureCollection = mapper.createObjectNode();
              featureCollection.put("type", "FeatureCollection");
              featureCollection.set("features", entry.getValue());
              limitedGeojsonCollections.put(entry.getKey(), mapper.convertValue(featureCollection, Object.class));
         }
         result.put("geojsonByGeometry", limitedGeojsonCollections);
         
         String normalizedFirstGeometryType = normalizeGeometryType(firstGeometryType);
         result.put("geometryType", normalizedFirstGeometryType);
         result.put("geometryFamily", resolveGeometryFamily(normalizedFirstGeometryType));
         result.put("geometryCounts", geometryCounts); // limit 전 전체 카운트
         result.put("limitedTotalCount", limitedAllRows.size());
         result.put("coordinate", effectiveCoordinate);
         
         return result;
    }
    
    private void validateGeometrySelection(String requestedSelection, Map<String, Integer> detectedFamilies) {
        if (detectedFamilies == null || detectedFamilies.isEmpty()) {
            throw new RecipeException(ErrorCode.INVALID_GEOMETRY_TYPE, "DXF 파일에서 지오메트리 타입을 찾을 수 없습니다.");
        }

        List<String> availableFamilies = detectedFamilies.entrySet().stream()
                .filter(entry -> entry.getValue() != null && entry.getValue() > 0)
                .map(Map.Entry::getKey)
                .filter(family -> !"UNKNOWN".equals(family))
                .collect(Collectors.toList());

        String normalizedSelection = normalizeGeometrySelection(requestedSelection);

        if (normalizedSelection == null || "ALL".equals(normalizedSelection)) {
            if (availableFamilies.size() > 1) {
                throw new RecipeException(ErrorCode.INVALID_GEOMETRY_TYPE, 
                    "DXF 레이어에 서로 다른 지오메트리 타입이 포함되어 있어 '전체'로 업로드할 수 없습니다.\n"
                    + "다음 타입 중 하나를 선택해주세요: " + String.join(", ", availableFamilies));
            }
        } else if (!availableFamilies.contains(normalizedSelection)) {
             throw new RecipeException(ErrorCode.INVALID_GEOMETRY_TYPE, 
                 String.format("선택한 지오메트리 타입(%s)에 해당하는 DXF 피처가 없습니다.", normalizedSelection));
        }
    }

    private boolean containsCoordinateProjectionError(String errorLog) {
        if (errorLog == null) return false;
        String lower = errorLog.toLowerCase();
        return lower.contains("invalid latitude")
                || lower.contains("invalid longitude")
                || lower.contains("failed to reproject")
                || lower.contains("out of source or destination srs");
    }

    private String buildFriendlyCoordinateError(String errorLog, String fallback) {
        if (containsCoordinateProjectionError(errorLog)) {
            return "선택한 좌표계와 DXF 데이터의 좌표가 일치하지 않습니다.\n"
                 + "DXF 데이터가 사용하는 EPSG 코드를 확인한 뒤 다시 선택해주세요.";
        }
        return fallback;
    }

    private String resolveGeometryFamily(String geometryType) {
        if (geometryType == null) {
            return "UNKNOWN";
        }
        String upper = geometryType.toUpperCase();
        if (upper.contains("POINT")) {
            return "POINT";
        }
        if (upper.contains("LINE")) {
            return "LINE";
        }
        if (upper.contains("POLYGON")
                || upper.contains("SURFACE")
                || upper.contains("FACE")
                || upper.contains("AREA")) {
            return "POLYGON";
        }
        return "UNKNOWN";
    }

    private String normalizeGeometryType(String geometryType) {
        if (geometryType == null) {
            return null;
        }
        String upper = geometryType.toUpperCase();
        if (upper.contains("POLYGON")
                || upper.contains("SURFACE")
                || upper.contains("FACE")
                || upper.contains("AREA")) {
            return "POLYGON";
        }
        if (upper.contains("LINE")) {
            return "LINESTRING";
        }
        if (upper.contains("POINT")) {
            return "POINT";
        }
        return upper;
    }

    private String normalizeGeometrySelection(String selection) {
        if (selection == null) {
            return null;
        }
        String upper = selection.trim().toUpperCase();
        if (upper.isEmpty()) {
            return null;
        }
        if ("ALL".equals(upper)) {
            return "ALL";
        }
        if (upper.contains("POINT")) {
            return "POINT";
        }
        if (upper.contains("LINE")) {
            return "LINE";
        }
        if (upper.contains("POLYGON")
                || upper.contains("SURFACE")
                || upper.contains("FACE")
                || upper.contains("AREA")) {
            return "POLYGON";
        }
        return upper;
    }

    private String getGeometryType(JsonNode feature) {
        JsonNode geometry = feature.get("geometry");
        if (geometry != null && geometry.has("type")) {
            return geometry.get("type").asText();
        }
        return null;
    }

    private Map<String, Integer> collectGeometryFamilies(JsonNode featuresArray) {
        Map<String, Integer> familyCounts = new LinkedHashMap<>();
        for (JsonNode featureNode : featuresArray) {
            String geometryType = getGeometryType(featureNode);
            String family = resolveGeometryFamily(geometryType);
            familyCounts.merge(family, 1, Integer::sum);
        }
        return familyCounts;
    }

    private Map<String, Object> resolveGeometrySelection(
            String requestedSelection,
            Map<String, Integer> detectedFamilies) {

        Map<String, Object> result = new HashMap<>();
        if (detectedFamilies == null || detectedFamilies.isEmpty()) {
            result.put("valid", false);
            result.put("status", "error");
            result.put("message", "DXF 파일에서 지오메트리 타입을 찾을 수 없습니다.");
            return result;
        }

        List<String> availableFamilies = detectedFamilies.entrySet().stream()
                .filter(entry -> entry.getValue() != null && entry.getValue() > 0)
                .map(Map.Entry::getKey)
                .filter(family -> !"UNKNOWN".equals(family))
                .collect(Collectors.toList());

        String normalizedSelection = normalizeGeometrySelection(requestedSelection);

        if (normalizedSelection == null || "ALL".equals(normalizedSelection)) {
            if (availableFamilies.size() > 1) {
                result.put("valid", false);
                result.put("status", "error");
                result.put("multipleGeometryTypes", true);
                result.put("detectedTypes", detectedFamilies);
                result.put("message", "DXF 레이어에 서로 다른 지오메트리 타입이 포함되어 있어 '전체'로 업로드할 수 없습니다.\n"
                        + "다음 타입 중 하나를 선택해주세요: " + String.join(", ", availableFamilies));
                return result;
            }
            normalizedSelection = availableFamilies.isEmpty() ? "UNKNOWN" : availableFamilies.get(0);
        } else if (!availableFamilies.contains(normalizedSelection)) {
            result.put("valid", false);
            result.put("status", "error");
            result.put("detectedTypes", detectedFamilies);
            result.put("message", String.format("선택한 지오메트리 타입(%s)에 해당하는 DXF 피처가 없습니다.", normalizedSelection));
            return result;
        }

        result.put("valid", true);
        result.put("resolvedSelection", normalizedSelection);
        result.put("detectedTypes", detectedFamilies);
        return result;
    }

    private ArrayNode filterFeaturesBySelection(
            JsonNode rawFeaturesArray,
            ObjectMapper mapper,
            Path sourceFilePath,
            Path tempDir,
            String coordinate,
            String layerName,
            String geometrySelection,
            Map<String, Integer> geometryFamilyCounts) throws IOException, InterruptedException {

        boolean acceptAll = geometrySelection == null || "ALL".equals(geometrySelection);
        ArrayNode filtered = mapper.createArrayNode();

        for (JsonNode featureNode : rawFeaturesArray) {
            String geometryType = getGeometryType(featureNode);
            String family = resolveGeometryFamily(geometryType);
            if (acceptAll || geometrySelection.equals(family)) {
                filtered.add(featureNode);
            }
        }

        if (filtered.isEmpty() && "POLYGON".equals(geometrySelection)) {
            ArrayNode forcedPolygons = runForcedGeometryConversion(
                    mapper,
                    sourceFilePath,
                    tempDir,
                    coordinate,
                    layerName,
                    "MULTIPOLYGON");
            if (forcedPolygons != null && !forcedPolygons.isEmpty()) {
                geometryFamilyCounts.put("POLYGON", forcedPolygons.size());
                return forcedPolygons;
            }
        }
        return filtered;
    }

    private ArrayNode runForcedGeometryConversion(
            ObjectMapper mapper,
            Path sourceFilePath,
            Path tempDir,
            String coordinate,
            String layerName,
            String forcedGeometryType) throws IOException, InterruptedException {

        Path forcedOutputPath = tempDir.resolve("dxf_forced_" + forcedGeometryType + "_" + UUID.randomUUID() + ".geojson");
        try {
            ProcessBuilder builder = makeCommand(
                    sourceFilePath.toString(),
                    forcedOutputPath.toString(),
                    coordinate,
                    layerName,
                    forcedGeometryType);
            Process process = builder.start();
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                return null;
            }
            String geoJsonContent = Files.readString(forcedOutputPath, StandardCharsets.UTF_8);
            JsonNode rootNode = mapper.readTree(geoJsonContent);
            JsonNode featuresArray = rootNode.path("features");
            if (!featuresArray.isArray() || featuresArray.size() == 0) {
                return null;
            }
            ArrayNode result = mapper.createArrayNode();
            for (JsonNode featureNode : featuresArray) {
                result.add(featureNode.deepCopy());
            }
            return result;
        } finally {
            try {
                Files.deleteIfExists(forcedOutputPath);
            } catch (IOException ignore) {
                logger.warn(ignore.getMessage(), ignore);
            }
        }
    }

    private Envelope extractOriginalEnvelope(Path tempDir, Path sourceFilePath, String layerName, int limit) {
        Path originalOutputPath = tempDir.resolve("dxf_preview_original_" + UUID.randomUUID() + ".geojson");
        try {
            ProcessBuilder command = buildPreviewCommand(
                    sourceFilePath.toString(),
                    originalOutputPath.toString(),
                    "",
                    layerName,
                    limit,
                    null
            );
            Process process = command.start();
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                logger.warn("원본 DXF 좌표 범위를 추출하지 못했습니다. exitCode={}", exitCode);
                return null;
            }

            String geoJsonContent = Files.readString(originalOutputPath, StandardCharsets.UTF_8);
            JsonNode rootNode = objectMapper.readTree(geoJsonContent);
            JsonNode featuresArray = rootNode.path("features");
            if (!featuresArray.isArray() || featuresArray.size() == 0) {
                return null;
            }

            GeometryJSON geometryJSON = new GeometryJSON();
            Envelope envelope = new Envelope();
            for (JsonNode featureNode : featuresArray) {
                JsonNode geometryNode = featureNode.get("geometry");
                if (geometryNode == null || geometryNode.isNull()) {
                    continue;
                }
                try (StringReader geoReader = new StringReader(geometryNode.toString())) {
                    Geometry geometry = geometryJSON.read(geoReader);
                    if (geometry != null && !geometry.isEmpty()) {
                        envelope.expandToInclude(geometry.getEnvelopeInternal());
                    }
                }
            }

            return envelope.isNull() ? null : envelope;
        } catch (Exception e) {
            logger.warn("원본 DXF 좌표 범위 추출 중 오류", e);
            return null;
        } finally {
        	deleteTempFile(originalOutputPath);
        }
    }

    private ProcessBuilder dxfCommand(String inputfile, String outputfile, String coordinate, String layerName, String forcedGeometryType) {
        return makeCommand(inputfile, outputfile, coordinate, layerName, forcedGeometryType);
    }
    
    private ProcessBuilder makeCommand(String inputfile, String outputfile, String coordinate, String layerName, String forcedGeometryType) {
        List<String> command = new ArrayList<>();
        command.add("ogr2ogr");
        command.add("-f");
        command.add("GeoJSON");
        String trimmedCoordinate = coordinate == null ? "" : coordinate.trim();
        if (!trimmedCoordinate.isEmpty()) {
            command.add("-s_srs");
            command.add(trimmedCoordinate);
            command.add("-t_srs");
            command.add(trimmedCoordinate);
        }
        command.add("-preserve_fid");
        if (forcedGeometryType != null && !forcedGeometryType.trim().isEmpty()) {
            command.add("-nlt");
            command.add(forcedGeometryType.trim());
        }
        command.add(outputfile);
        command.add(inputfile);
        if (layerName != null && !layerName.trim().isEmpty()) {
            command.add(layerName.trim());
        }
        return new ProcessBuilder(command);
    }

    private ProcessBuilder buildPreviewCommand(String inputfile, String outputfile, String coordinate, String layerName, int limit, String forcedGeometryType) {
        List<String> command = new ArrayList<>();
        command.add("ogr2ogr");
        command.add("-f");
        command.add("GeoJSON");
        String trimmedCoordinate = coordinate == null ? "" : coordinate.trim();
        if (!trimmedCoordinate.isEmpty()) {
            command.add("-s_srs");
            command.add(trimmedCoordinate);
            command.add("-t_srs");
            command.add("EPSG:3857");
        }
        command.add("-preserve_fid");
        if (forcedGeometryType != null && !forcedGeometryType.isBlank()) {
            command.add("-nlt");
            command.add(forcedGeometryType);
        }
        command.add("-limit");
        command.add(String.valueOf(Math.max(limit, 1)));
        command.add("-unsetFid");
        command.add(outputfile);
        command.add(inputfile);
        if (layerName != null && !layerName.trim().isEmpty()) {
            command.add(layerName);
        }
        return new ProcessBuilder(command);
    }

    private String resolveForcedGeometryTypeForUpload(String requestedType) {
        String normalized = normalizeGeometryType(requestedType);
        if (normalized == null) {
            return null;
        }
        switch (normalized) {
            case "POLYGON":
                return "MULTIPOLYGON";
            case "LINESTRING":
                return "MULTILINESTRING";
            case "POINT":
                return "POINT";
            default:
                return null;
        }
    }

    private String determineSelectedGeometryType(String normalizedSelectedType,
                                                 String originalSelectedType,
                                                 String resolvedSelection) {
        if (normalizedSelectedType != null) {
            return normalizedSelectedType;
        }
        String normalizedOriginal = normalizeGeometryType(originalSelectedType);
        if (normalizedOriginal != null) {
            return normalizedOriginal;
        }
        return mapGeometryFamilyToType(resolvedSelection);
    }

    private String mapGeometryFamilyToType(String geometryFamily) {
        if (geometryFamily == null) {
            return null;
        }
        switch (geometryFamily) {
            case "POINT":
                return "POINT";
            case "LINE":
                return "LINESTRING";
            case "POLYGON":
                return "POLYGON";
            default:
                return null;
        }
    }

    private String resolveTableGeometryType(String actualRawGeometryType, String forcedGeometryType) {
        if (forcedGeometryType != null && !forcedGeometryType.isBlank()) {
            return forcedGeometryType.toUpperCase();
        }
        if (actualRawGeometryType == null || actualRawGeometryType.isBlank()) {
            return "GEOMETRY";
        }
        return actualRawGeometryType.toUpperCase();
    }
    
    private void deleteTempFile(Path path) {
        if (path != null) {
            try {
                Files.deleteIfExists(path);
                if (Files.exists(path)) {
                    logger.error("파일 삭제 실패 : {}", path);
                } else {
                    logger.info("파일 삭제 확인됨: {}", path);
                }
            } catch (IOException e) {
                logger.warn("임시 파일 삭제 실패: {}", path, e);
            }
        }
    }

}