package incheon.ags.mrb.upload.service;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonMappingException;
import org.geotools.api.data.DataStore;
import org.geotools.api.data.DataStoreFinder;
import org.geotools.api.data.Query;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.feature.Property;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.api.referencing.FactoryException;
import org.geotools.api.referencing.operation.TransformException;
import org.geotools.data.DataUtilities;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.geometry.jts.JTS;
import org.geotools.feature.FeatureIterator;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.feature.FeatureJSON;
import org.geotools.referencing.CRS;
import com.fasterxml.jackson.databind.ObjectMapper;

import incheon.ags.mrb.main.exception.ErrorCode;
import incheon.ags.mrb.main.exception.RecipeException;

import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Envelope;
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 incheon.ags.mrb.upload.mapper.ProcessFileMapper;
import incheon.ags.mrb.upload.service.util.CoordinateValidationUtil;
import incheon.ags.mrb.upload.vo.FileUploadRequestDTO;

@Service
public class ProcessShapeFileService {

    private static final Logger logger = LoggerFactory.getLogger(ProcessShapeFileService.class);

    private final FileProcessUtilService fileProcessUtilService;
    private final TableCreationService tableCreationService;
    private final DataInsertionService dataInsertionService;
    private final ProcessFileMapper processFileMapper;

    @Autowired
    public ProcessShapeFileService(
        FileProcessUtilService fileProcessUtilService,
        TableCreationService tableCreationService,
        DataInsertionService dataInsertionService,
        ProcessFileMapper processFileMapper
    ) {
        this.fileProcessUtilService = fileProcessUtilService;
        this.tableCreationService = tableCreationService;
        this.dataInsertionService = dataInsertionService;
        this.processFileMapper = processFileMapper;
    }

    // ========================================================================
    // 1. Shape 파일 업로드 (최종 저장)
    // ========================================================================
    public Map<String, Object> processShapeFile(FileUploadRequestDTO dto) throws IOException {
        Path tempDir = null;
        try {
            tempDir = fileProcessUtilService.createTempDirectory();
            fileProcessUtilService.extractZipFile(dto.getFile(), tempDir);
            validateRequiredFilesExist(tempDir);
            
            Path shpFile = fileProcessUtilService.findShapeFile(tempDir);
            
            if (shpFile == null) {
                throw new RecipeException(ErrorCode.REQUIRED_FILE_MISSING, "압축 파일 내에서 .shp 파일을 찾을 수 없습니다.");
            }
            
            return processWithGeoTools(shpFile, dto);
            
        } finally {
            if (tempDir != null) {
                fileProcessUtilService.cleanupTempDirectory(tempDir);
            }
        }
    }

    private Map<String, Object> processWithGeoTools(Path shpFile, FileUploadRequestDTO dto) throws IOException {
        String postGISTableName = fileProcessUtilService.generateUniqueTableName();
        DataStore shapeDataStore = null;
        
        try {
            File file = shpFile.toFile();
            Map<String, Object> shapeParams = new HashMap<>();
            shapeParams.put("url", file.toURI().toURL());
            if (dto.getEncoding() != null) {
                shapeParams.put("charset", dto.getEncoding());
            }
            
            shapeDataStore = DataStoreFinder.getDataStore(shapeParams);
            if (shapeDataStore == null) {
                throw new RecipeException(ErrorCode.FILE_PARSE_FAILED, "Shape 파일을 읽을 수 없습니다. 인코딩을 확인해주세요.");
            }

            String[] typeNames = shapeDataStore.getTypeNames();
            String typeName = typeNames[0];
            SimpleFeatureSource source = shapeDataStore.getFeatureSource(typeName);
            SimpleFeatureCollection collection = source.getFeatures();
            SimpleFeatureType schema = source.getSchema();

            // 사용자 선택 좌표계 (PRJ 무시)
            int srid = fileProcessUtilService.extractSridFromCoordinate(dto.getFilecoordinate());
            logger.info("[업로드] 사용자 선택 SRID: EPSG:{} (PRJ 파일 무시)", srid);
            
            // 테이블 생성
            tableCreationService.createTable(postGISTableName, schema, srid);
            
            // 데이터 삽입
            Map<String, Object> insertResult = dataInsertionService.insertData(postGISTableName, srid, collection, dto);
            
            Map<String, Object> result = new HashMap<>();
            result.put("insertedCount", insertResult.get("insertedCount"));
            result.put("safeTableName", postGISTableName);
            result.put("srid", srid);
            
            return result;
            
        } finally {
            if (shapeDataStore != null) {
                shapeDataStore.dispose();
            }
        }
    }

    // ========================================================================
    // 2. Shape 파일 컬럼 추출 (미리보기 1단계)
    // ========================================================================
    public Map<String, Object> getShapeColumns(MultipartFile file, int previewCounter, String encoding) throws Exception {
        Path tempDir = null;
        DataStore shapeDataStore = null;
        
        try {
            tempDir = fileProcessUtilService.createTempDirectory();
            fileProcessUtilService.extractZipFile(file, tempDir);
            validateRequiredFilesExist(tempDir);
            
            // 파일명 일치 여부 확인
            validateShapeFileNames(tempDir);

            Path shpFile = fileProcessUtilService.findShapeFile(tempDir);
            
            if (shpFile == null) {
                throw new RecipeException(ErrorCode.REQUIRED_FILE_MISSING, 
                    "압축 파일 내에서 .shp 파일을 찾을 수 없습니다.\n폴더 구조 없이 파일들을 바로 압축했는지 확인해주세요.");
            }
            
            File shapeFile = shpFile.toFile();
            
            Map<String, Object> shapeParams = new HashMap<>();
            shapeParams.put("url", shapeFile.toURI().toURL());
            if (encoding != null) {
                shapeParams.put("charset", encoding);
            }
            
            shapeDataStore = DataStoreFinder.getDataStore(shapeParams);
                
            if (shapeDataStore == null) {
                throw new RecipeException(ErrorCode.ENCODING_ERROR, 
                    "Shapefile을 읽을 수 없습니다.\n인코딩 설정이나 파일 손상 여부를 확인해주세요.");
            }

            String[] typeNames = shapeDataStore.getTypeNames();
            if (typeNames.length == 0) {
                throw new RecipeException(ErrorCode.FILE_EMPTY, "Shapefile 내에 유효한 데이터 레이어가 없습니다.");
            }

            String typeName = typeNames[0];
            SimpleFeatureSource source = shapeDataStore.getFeatureSource(typeName);
            SimpleFeatureType schema = source.getSchema();
            
            // 컬럼 정보 추출
            List<String> columns = new ArrayList<>();
            for (int i = 0; i < schema.getAttributeCount(); i++) {
                // Geometry 타입 제외하고 일반 속성만 추출
                if (!Geometry.class.isAssignableFrom(schema.getDescriptor(i).getType().getBinding())) {
                    columns.add(schema.getDescriptor(i).getLocalName());
                }
            }
            
            // 행 데이터 추출
            List<Map<String, Object>> rows = new ArrayList<>();
            Query previewQuery = new Query(typeName);
            previewQuery.setMaxFeatures(previewCounter);
            SimpleFeatureCollection collection = source.getFeatures(previewQuery);
            
            try (FeatureIterator<SimpleFeature> features = collection.features()) {
                while (features.hasNext()) {
                    SimpleFeature feature = features.next();
                    Map<String, Object> row = new HashMap<>();
                    for (Property prop : feature.getProperties()) {
                        if (!(prop.getValue() instanceof Geometry) && prop.getValue() != null) {
                            row.put(prop.getName().getLocalPart(), prop.getValue().toString());
                        }
                    }
                    rows.add(row);
                }
            }
            
            Map<String, Object> result = new HashMap<>();
            result.put("columns", columns);
            result.put("rows", rows);
            return result;

        } catch (IOException e) {
             throw new RecipeException(ErrorCode.FILE_PARSE_FAILED, "Shape 파일 처리 중 오류: " + e.getMessage());
        } finally {
            if (shapeDataStore != null) shapeDataStore.dispose();
            if (tempDir != null) fileProcessUtilService.cleanupTempDirectory(tempDir);
        }
    }
    @Autowired private ObjectMapper objectMapper;

    // ========================================================================
    // 3. Shape 파일 미리보기 (지도 표시)
    // ========================================================================
    public Map<String, Object> previewShapeFile(MultipartFile file, int previewCounter, String coordinate, String encoding) throws IOException, FactoryException {
        Path tempDir = null;
        DataStore shapeDataStore = null;
        
        try {
            tempDir = fileProcessUtilService.createTempDirectory();
            fileProcessUtilService.extractZipFile(file, tempDir);
            validateRequiredFilesExist(tempDir);
            
            Path shpFile = fileProcessUtilService.findShapeFile(tempDir);
            
            if (shpFile == null) {
                 throw new RecipeException(ErrorCode.REQUIRED_FILE_MISSING, ".shp 파일을 찾을 수 없습니다.");
            }
            
            File shapeFile = shpFile.toFile();
            
            Map<String, Object> shapeParams = new HashMap<>();
            shapeParams.put("url", shapeFile.toURI().toURL());
            if (encoding != null) {
                shapeParams.put("charset", encoding);
            }
            
            shapeDataStore = DataStoreFinder.getDataStore(shapeParams);
            if (shapeDataStore == null) {
                 throw new RecipeException(ErrorCode.ENCODING_ERROR, "Shapefile을 읽을 수 없습니다. 인코딩을 확인해주세요.");
            }

            String typeName = shapeDataStore.getTypeNames()[0];
            SimpleFeatureSource source = shapeDataStore.getFeatureSource(typeName);
            SimpleFeatureType schema = source.getSchema();
            
            // 사용자 선택 좌표계
            int detectedSrid = fileProcessUtilService.extractSridFromCoordinate(coordinate);
            logger.info("[미리보기] 사용자 선택 SRID: EPSG:{}", detectedSrid);
            
            Query previewQuery = new Query(typeName);
            previewQuery.setMaxFeatures(previewCounter);
            SimpleFeatureCollection collection = source.getFeatures(previewQuery);
            
            List<String> columns = new ArrayList<>();
            for (int i = 0; i < schema.getAttributeCount(); i++) {
                if (!Geometry.class.isAssignableFrom(schema.getDescriptor(i).getType().getBinding())) {
                    columns.add(schema.getDescriptor(i).getLocalName());
                }
            }
            
            List<Map<String, Object>> rows = new ArrayList<>();
            FeatureJSON fjson = new FeatureJSON();
            List<SimpleFeature> featureList = new ArrayList<>();
            Envelope envelope = new Envelope();
            Envelope originalEnvelope = new Envelope();
            boolean transformFailed = false;
            
            // 좌표 변환 준비 (detectedSrid -> 3857)
            org.geotools.api.referencing.operation.MathTransform transform = null;
            SimpleFeatureType transformedSchema = schema;

            if (detectedSrid != 3857) {
                org.geotools.api.referencing.crs.CoordinateReferenceSystem sourceCRS =
                        CRS.decode("EPSG:" + detectedSrid, true);
                org.geotools.api.referencing.crs.CoordinateReferenceSystem targetCRS =
                        CRS.decode("EPSG:3857", true);

                transform = CRS.findMathTransform(sourceCRS, targetCRS, true);

                SimpleFeatureTypeBuilder schemaBuilder = new SimpleFeatureTypeBuilder();
                schemaBuilder.init(schema);
                schemaBuilder.setCRS(targetCRS);
                transformedSchema = schemaBuilder.buildFeatureType();
            }
            
            try (FeatureIterator<SimpleFeature> features = collection.features()) {
                int count = 0;
                org.geotools.feature.simple.SimpleFeatureBuilder builder = 
                    new org.geotools.feature.simple.SimpleFeatureBuilder(transformedSchema);
                
                while (features.hasNext() && count < previewCounter) {
                    SimpleFeature feature = features.next();

                    Geometry originalGeom = (Geometry) feature.getDefaultGeometry();
                    if (originalGeom != null && !originalGeom.isEmpty()) {
                        originalEnvelope.expandToInclude(originalGeom.getEnvelopeInternal());
                    }

                    // 속성 추출
                    Map<String, Object> row = new HashMap<>();
                    for (Property prop : feature.getProperties()) {
                        if (!(prop.getValue() instanceof Geometry) && prop.getValue() != null) {
                            row.put(prop.getName().getLocalPart(), prop.getValue().toString());
                        }
                    }
                    rows.add(row);

                    // 좌표 변환 및 새 피처 생성
                    if (transform != null) {
                        try {
                            if (originalGeom != null) {
                                Geometry transformedGeom = JTS.transform(originalGeom, transform);
                                if (transformedGeom != null && !transformedGeom.isEmpty()) {
                                    envelope.expandToInclude(transformedGeom.getEnvelopeInternal());
                                }

                                builder.reset();
                                for (Property prop : feature.getProperties()) {
                                    if (prop.getValue() instanceof Geometry) {
                                        builder.set(prop.getName(), transformedGeom);
                                    } else {
                                        builder.set(prop.getName(), prop.getValue());
                                    }
                                }
                                featureList.add(builder.buildFeature(feature.getIdentifier().getID()));
                            } else {
                                featureList.add(feature);
                            }
                        } catch (TransformException e) {
                            logger.error("[미리보기] Geometry 변환 실패", e);
                            transformFailed = true;
                            featureList.add(feature); // 변환 실패 시 원본 유지
                        }
                    } else {
                        if (originalGeom != null && !originalGeom.isEmpty()) {
                            envelope.expandToInclude(originalGeom.getEnvelopeInternal());
                        }
                        featureList.add(feature);
                    }
                    count++;
                }
            }

            // 검증 로직 실행 (예외 발생 시 즉시 중단)
            validatePreviewEnvelope(originalEnvelope, envelope, detectedSrid, transform != null, transformFailed);
            
            if (featureList.isEmpty()) {
                throw new RecipeException(ErrorCode.INVALID_COORDINATE, 
                    "미리보기 좌표를 해석할 수 없습니다.\n좌표계 또는 데이터 값을 다시 확인해주세요.");
            }

            SimpleFeatureCollection previewFeatures = DataUtilities.collection(featureList);
            String geojsonString;
            try (StringWriter writer = new StringWriter()){
                fjson.writeFeatureCollection(previewFeatures, writer);
                geojsonString = writer.toString();
            }

            Object geojsonObject;
            try {
                geojsonObject = objectMapper.readValue(geojsonString, Object.class);
            } catch (JsonMappingException e) {
                geojsonObject = geojsonString;
            }
            
            Map<String, Object> result = new HashMap<>();
            result.put("columns", columns);
            result.put("rows", rows);
            result.put("geojson", geojsonObject);
            result.put("srid", detectedSrid);
            
            return result;
            
        } finally {
            if (shapeDataStore != null) shapeDataStore.dispose();
            if (tempDir != null) fileProcessUtilService.cleanupTempDirectory(tempDir);
        }
    }

    // ========================================================================
    // Helper: 검증 로직
    // ========================================================================
    private void validateShapeFileNames(Path tempDir) {
        File[] files = tempDir.toFile().listFiles();
        String baseName = null;
        if (files != null) {
            for (File f : files) {
                String name = f.getName();
                if (name.startsWith(".") || name.startsWith("__")) continue; 
                
                int lastDot = name.lastIndexOf(".");
                if (lastDot > 0) {
                    String ext = name.substring(lastDot + 1).toLowerCase();
                    if (List.of("shp", "dbf", "shx").contains(ext)) {
                        String currentBaseName = name.substring(0, lastDot);
                        if (baseName == null) {
                            baseName = currentBaseName;
                        } else if (!baseName.equals(currentBaseName)) {	
                             throw new RecipeException(ErrorCode.FILE_NAME_INVALID, 
                                 "파일명이 서로 다릅니다.\n모든 파일(.shp, .dbf 등)의 이름을 동일하게 맞춰주세요.");
                        }
                    }
                }
            }
        }
    }
    
    // [추가] 필수 파일(.shp, .shx, .dbf) 존재 여부 강제 확인
    private void validateRequiredFilesExist(Path tempDir) {
        File dir = tempDir.toFile();
        boolean hasShp = false;
        boolean hasShx = false;
        boolean hasDbf = false;

        File[] files = dir.listFiles();
        if (files != null) {
            for (File f : files) {
                String name = f.getName().toLowerCase();
                if (name.startsWith(".") || name.startsWith("__")) continue; // 맥(Mac) 등 숨김파일 무시
                
                if (name.endsWith(".shp")) hasShp = true;
                if (name.endsWith(".shx")) hasShx = true;
                if (name.endsWith(".dbf")) hasDbf = true;
            }
        }

        if (!hasShp) {
            throw new RecipeException(ErrorCode.REQUIRED_FILE_MISSING, "압축 파일 내에 .shp 파일이 누락되었습니다.");
        }
        if (!hasShx) {
            throw new RecipeException(ErrorCode.REQUIRED_FILE_MISSING, "압축 파일 내에 .shx 파일이 누락되었습니다.");
        }
        if (!hasDbf) {
            throw new RecipeException(ErrorCode.REQUIRED_FILE_MISSING, "압축 파일 내에 .dbf 파일이 누락되었습니다.");
        }
    }

    private void validatePreviewEnvelope(Envelope original, Envelope transformed, int srid, boolean transformedUsed, boolean failed) {
        // SRID 일관성 체크
        if (!CoordinateValidationUtil.isEnvelopeConsistentWithSrid(original, srid)) {
            String message = (srid > 0)
                    ? String.format("선택한 좌표계(EPSG:%d)와 데이터 좌표 범위가 일치하지 않습니다.\n데이터 좌표계를 확인해주세요.", srid)
                    : "선택한 좌표계와 데이터 좌표 범위가 일치하지 않습니다.";
            throw new RecipeException(ErrorCode.INVALID_COORDINATE, message);
        }

        // 변환 실패 및 범위 이탈 체크
        if (transformedUsed) {
            if (failed || transformed.isNull()) {
                throw new RecipeException(ErrorCode.INVALID_COORDINATE, 
                    String.format("선택한 좌표계(EPSG:%d)로 좌표를 변환할 수 없습니다.\n데이터의 실제 좌표계를 다시 확인해주세요.", srid));
            } else if (!CoordinateValidationUtil.isReasonableEnvelope(transformed)
                    || !CoordinateValidationUtil.isWithinKorea(transformed)) {
                throw new RecipeException(ErrorCode.OUT_OF_KOREA_BOUNDS, 
                    "선택한 좌표가 대한민국 지도 서비스 영역을 벗어났습니다.\n올바른 좌표계를 선택했는지 확인해주세요.");
            }
        } else {
            // 변환 없이 사용하는 경우 (이미 3857 등)
            if (original.isNull()
                    || !CoordinateValidationUtil.isReasonableEnvelope(original)
                    || !CoordinateValidationUtil.isWithinKorea(original)) {
                throw new RecipeException(ErrorCode.OUT_OF_KOREA_BOUNDS, 
                    "선택한 좌표가 대한민국 지도 서비스 영역을 벗어났습니다.\n올바른 좌표계를 선택했는지 확인해주세요.");
            }
        }

        // 4326인데 위경도 범위가 아닌 경우
        if (srid == 4326 && !CoordinateValidationUtil.isLatLonEnvelope(original)) {
            throw new RecipeException(ErrorCode.INVALID_COORDINATE, 
                "선택한 좌표계(EPSG:4326) 범위에 맞지 않는 좌표 값이 포함되어 있습니다.\n데이터가 위경도 좌표인지 확인해주세요.");
        }
    }
}