package incheon.uis.ums.shp;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.geotools.api.data.Transaction;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.api.data.FeatureWriter;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.geom.GeometryJSON;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * GeoJSON 문자열을 SHP 파일로 변환하는 유틸리티
 */
public class GeoJsonToShpConverter {

    private static final Logger log = LoggerFactory.getLogger(GeoJsonToShpConverter.class);
    private static final String[] SHAPEFILE_EXTENSIONS = {".shp", ".shx", ".dbf", ".prj", ".cpg"};
    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * GeoJSON 문자열을 SHP ZIP 파일로 변환
     *
     * @param geojsonStr GeoJSON 문자열
     * @param tempDir    임시 디렉토리
     * @param layerName  레이어 이름
     * @param srid       좌표계 SRID (기본값 5186)
     * @return 생성된 ZIP 파일 경로
     */
    public static Path convert(String geojsonStr, Path tempDir, String layerName, int srid) throws Exception {
        return convert(geojsonStr, tempDir, layerName, srid, null);
    }

    public static Path convert(String geojsonStr, Path tempDir, String layerName, int srid,
                               WfsSchemaUtil.SchemaInfo schemaInfo) throws Exception {
        // GeoJSON 파싱
        JsonNode rootNode = objectMapper.readTree(geojsonStr);

        if (!"FeatureCollection".equals(rootNode.path("type").asText())) {
            throw new IllegalArgumentException("GeoJSON must be a FeatureCollection");
        }

        JsonNode featuresNode = rootNode.path("features");
        boolean hasFeatures = featuresNode.isArray() && !featuresNode.isEmpty();

        // schemaInfo 없이 피처도 없으면 스키마 생성 불가
        if (!hasFeatures && schemaInfo == null) {
            throw new IllegalArgumentException("No features in GeoJSON");
        }

        // 좌표계 설정
        CoordinateReferenceSystem crs = CRS.decode("EPSG:" + srid);

        // 지오메트리 클래스 결정
        Class<? extends Geometry> geometryClass;
        if (schemaInfo != null) {
            geometryClass = schemaInfo.geometryClass;
        } else {
            String geomType = featuresNode.get(0).path("geometry").path("type").asText();
            geometryClass = determineGeometryClass(geomType);
        }

        // SimpleFeatureType 빌드
        SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
        builder.setName(layerName);
        builder.setCRS(crs);
        builder.add("the_geom", geometryClass);

        // 속성 필드 추가 (최대 10자로 제한 - SHP 제약)
        Map<String, String> fieldMapping = new LinkedHashMap<>();

        if (schemaInfo != null) {
            // DescribeFeatureType 기반 스키마 사용 (DB 컬럼과 동일하게)
            for (FieldDef fd : schemaInfo.attributeFields) {
                String shpFieldName = ShpColumnMapping.fullToShp(layerName, fd.name);
                int suffix = 1;
                String originalShpFieldName = shpFieldName;
                while (fieldMapping.containsValue(shpFieldName)) {
                    shpFieldName = originalShpFieldName.substring(0, Math.min(8, originalShpFieldName.length())) + suffix++;
                }
                fieldMapping.put(fd.name, shpFieldName);
                builder.add(shpFieldName, fd.javaType);
            }
        } else {
            // fallback: 전체 피처 순회하여 필드 수집
            Set<String> allFieldNames = new LinkedHashSet<>();
            for (JsonNode featureNode : featuresNode) {
                JsonNode propsNode = featureNode.path("properties");
                if (propsNode.isObject()) {
                    propsNode.fieldNames().forEachRemaining(allFieldNames::add);
                }
            }
            for (String fieldName : allFieldNames) {
                String shpFieldName = ShpColumnMapping.fullToShp(layerName, fieldName);
                int suffix = 1;
                String originalShpFieldName = shpFieldName;
                while (fieldMapping.containsValue(shpFieldName)) {
                    shpFieldName = originalShpFieldName.substring(0, Math.min(8, originalShpFieldName.length())) + suffix++;
                }
                fieldMapping.put(fieldName, shpFieldName);
                builder.add(shpFieldName, String.class);
            }
        }

        SimpleFeatureType featureType = builder.buildFeatureType();

        // SHP 파일 생성
        Path shapefilePath = tempDir.resolve(layerName + ".shp");
        File shapeFile = shapefilePath.toFile();

        Map<String, Serializable> params = new HashMap<>();
        params.put("url", shapeFile.toURI().toURL());
        params.put("create spatial index", Boolean.TRUE);

        ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory();
        ShapefileDataStore dataStore = (ShapefileDataStore) factory.createNewDataStore(params);
        dataStore.createSchema(featureType);
        dataStore.setCharset(StandardCharsets.UTF_8);

        Transaction transaction = new DefaultTransaction("create");
        GeometryJSON geometryJSON = new GeometryJSON(15); // 15자리 정밀도

        try (FeatureWriter<SimpleFeatureType, SimpleFeature> writer =
                     dataStore.getFeatureWriter(dataStore.getTypeNames()[0], transaction)) {

            for (JsonNode featureNode : featuresNode) {
                JsonNode geomNode = featureNode.path("geometry");
                JsonNode featurePropsNode = featureNode.path("properties");

                if (geomNode.isMissingNode() || geomNode.isNull()) {
                    continue;
                }

                try {
                    // GeoJSON geometry → JTS Geometry
                    String geomJsonStr = objectMapper.writeValueAsString(geomNode);
                    Geometry geometry = geometryJSON.read(new StringReader(geomJsonStr));

                    if (geometry == null) {
                        continue;
                    }

                    SimpleFeature feature = writer.next();
                    feature.setDefaultGeometry(geometry);

                    // 속성 설정
                    for (Map.Entry<String, String> entry : fieldMapping.entrySet()) {
                        String originalName = entry.getKey();
                        String shpName = entry.getValue();
                        JsonNode valueNode = featurePropsNode.path(originalName);

                        if (valueNode.isMissingNode() || valueNode.isNull()) {
                            feature.setAttribute(shpName, null);
                            continue;
                        }

                        // schemaInfo 기반일 때 타입 맞춰 변환
                        Class<?> fieldType = schemaInfo != null
                                ? getFieldType(schemaInfo.attributeFields, originalName)
                                : String.class;

                        try {
                            if (fieldType == Double.class) {
                                feature.setAttribute(shpName, valueNode.asDouble());
                            } else if (fieldType == Integer.class) {
                                feature.setAttribute(shpName, valueNode.asInt());
                            } else if (fieldType == Long.class) {
                                feature.setAttribute(shpName, valueNode.asLong());
                            } else {
                                String value = valueNode.isTextual() ? valueNode.asText() : valueNode.toString();
                                if (value.length() > 254) value = value.substring(0, 254);
                                feature.setAttribute(shpName, value);
                            }
                        } catch (Exception e) {
                            // 타입 변환 실패 시 String으로 fallback
                            String value = valueNode.isTextual() ? valueNode.asText() : valueNode.toString();
                            if (value.length() > 254) value = value.substring(0, 254);
                            feature.setAttribute(shpName, value);
                        }
                    }

                    writer.write();

                } catch (Exception e) {
                    log.warn("Feature 처리 실패, 건너뜀: {}", e.getMessage());
                }
            }

            transaction.commit();
            log.info("SHP 파일 생성 완료: {}", shapefilePath);

        } catch (Exception e) {
            transaction.rollback();
            throw e;
        } finally {
            transaction.close();
            dataStore.dispose();
        }

        // ZIP 파일 생성
        return createShapefileZip(tempDir, layerName);
    }

    private static Class<?> getFieldType(List<FieldDef> fields, String fieldName) {
        for (FieldDef fd : fields) {
            if (fd.name.equals(fieldName)) return fd.javaType;
        }
        return String.class;
    }

    /**
     * 지오메트리 타입 문자열 → JTS Geometry 클래스
     */
    private static Class<? extends Geometry> determineGeometryClass(String geomType) {
        if (geomType == null) {
            return Geometry.class;
        }

        switch (geomType.toUpperCase()) {
            case "POINT":
                return Point.class;
            case "LINESTRING":
                return LineString.class;
            case "POLYGON":
                return Polygon.class;
            case "MULTIPOINT":
                return MultiPoint.class;
            case "MULTILINESTRING":
                return MultiLineString.class;
            case "MULTIPOLYGON":
                return MultiPolygon.class;
            default:
                return Geometry.class;
        }
    }

    /**
     * SHP 관련 파일들을 ZIP으로 압축
     */
    private static Path createShapefileZip(Path tempDir, String layerName) throws IOException {
        Path zipFile = tempDir.resolve(layerName + ".zip");

        List<Path> filesToZip = new ArrayList<>();
        for (String extension : SHAPEFILE_EXTENSIONS) {
            Path componentFile = tempDir.resolve(layerName + extension);
            if (Files.exists(componentFile)) {
                filesToZip.add(componentFile);
            }
        }

        if (filesToZip.isEmpty()) {
            throw new IOException("압축할 Shapefile 컴포넌트를 찾을 수 없습니다.");
        }

        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipFile, StandardOpenOption.CREATE))) {
            for (Path componentFile : filesToZip) {
                String entryName = componentFile.getFileName().toString();
                ZipEntry entry = new ZipEntry(entryName);
                zos.putNextEntry(entry);

                try (InputStream fis = Files.newInputStream(componentFile)) {
                    byte[] buffer = new byte[8192];
                    int bytesRead;
                    while ((bytesRead = fis.read(buffer)) != -1) {
                        zos.write(buffer, 0, bytesRead);
                    }
                }
                zos.closeEntry();
            }
            zos.flush();
        }

        log.info("ZIP 파일 생성 완료: {}, 크기: {} bytes", zipFile, Files.size(zipFile));
        return zipFile;
    }

    /**
     * GeoJSON → SHP 변환 후 이중 ZIP 구조로 생성
     *
     * 구조:
     * - SHP 파일: {shpFileName}.shp, {shpFileName}.dbf, ...
     * - 첫번째 ZIP: {innerZipName}.zip (SHP 파일들 포함)
     * - 최종 ZIP: {outerZipName}.zip (첫번째 ZIP 포함)
     *
     * @param geojsonStr GeoJSON 문자열
     * @param tempDir 임시 디렉토리
     * @param shpFileName SHP 파일명 (예: "rdl_rdct_l")
     * @param innerZipName 첫번째 ZIP 이름 (예: "extracted_rdl_rdct_l")
     * @param outerZipName 최종 ZIP 이름 (예: "추출_도로중심선")
     * @param srid 좌표계 SRID
     * @return 최종 ZIP 파일 경로
     */
    public static Path convertWithDoubleZip(String geojsonStr, Path tempDir,
                                            String shpFileName, String innerZipName,
                                            String outerZipName, int srid) throws Exception {
        return convertWithDoubleZip(geojsonStr, tempDir, shpFileName, innerZipName, outerZipName, srid, null);
    }

    public static Path convertWithDoubleZip(String geojsonStr, Path tempDir,
                                            String shpFileName, String innerZipName,
                                            String outerZipName, int srid,
                                            WfsSchemaUtil.SchemaInfo schemaInfo) throws Exception {
        // 1. GeoJSON → SHP 변환 (shpFileName으로 SHP 파일 생성)
        Path innerZipFile = convert(geojsonStr, tempDir, shpFileName, srid, schemaInfo);

        // 2. 첫번째 ZIP 이름 변경 (shpFileName.zip → innerZipName.zip)
        Path renamedInnerZip = tempDir.resolve(innerZipName + ".zip");
        if (!innerZipFile.equals(renamedInnerZip)) {
            Files.move(innerZipFile, renamedInnerZip);
        }

        // 3. 최종 ZIP 생성 (innerZipName.zip을 포함)
        Path outerZipFile = tempDir.resolve(outerZipName + ".zip");
        try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(outerZipFile, StandardOpenOption.CREATE))) {
            String entryName = renamedInnerZip.getFileName().toString();
            ZipEntry entry = new ZipEntry(entryName);
            zos.putNextEntry(entry);

            try (InputStream fis = Files.newInputStream(renamedInnerZip)) {
                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = fis.read(buffer)) != -1) {
                    zos.write(buffer, 0, bytesRead);
                }
            }
            zos.closeEntry();
            zos.flush();
        }

        log.info("이중 ZIP 생성 완료: {} (내부: {})", outerZipFile, renamedInnerZip.getFileName());
        return outerZipFile;
    }
}