package incheon.ags.dss.util;

import org.geotools.api.data.DataStore;
import org.geotools.api.data.DataStoreFinder;
import org.geotools.api.data.FeatureSource;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.feature.simple.SimpleFeatureType;
import org.geotools.api.referencing.crs.CoordinateReferenceSystem;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.WKTWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

/**
 * GIS 파일(ana_stats_data_mst) 로더.
 * AbstractFileLoader의 구현체.
 */
@Profile("data-load-dss")
@Component
@Order(1)
public class GisFileLoader extends AbstractFileLoader {

    private static final Logger log = LoggerFactory.getLogger(GisFileLoader.class);

    @Value("${dss.loader.gis.src:data/dss/gis/src}")
    private String dssLoaderGisSrc;

    @Value("${dss.loader.gis.done:data/dss/gis/tmp}")
    private String dssLoaderGisTmp;

    /**
     * 파일 원본 좌표계 (현재 GIS 파일 데이터)
     */
    private static final int SOURCE_SRID = 5179;

    /**
     * DB 테이블 좌표계 (icdss.ana_stats_data_mst.geom)
     */
    private static final int TARGET_SRID = 5186;

    // --- GIS 처리용 헬퍼 객체 ---
    private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), SOURCE_SRID);
    private final WKTWriter wktWriter = new WKTWriter();
    private static final int CHUNK_SIZE = 100; // DB Batch 크기

    // ID 필드 우선순위 목록
    private static final List<String> ID_FIELD_CANDIDATES = List.of(
            "TOT_REG_CD",
            "GRID_1K_CD",
            "GRID_100M_"
    );

    // DB SQL
    private static final String INSERT_SQL =
            "INSERT INTO icdss.ana_stats_data_mst (scp_cd, scp_type, geom, frst_reg_id, last_mdfcn_id) " +
            "VALUES (?, ?, ST_Transform(ST_GeomFromText(?, " + SOURCE_SRID + "), " + TARGET_SRID + "), 'BATCH', 'BATCH') " +
            "ON CONFLICT (scp_cd) DO UPDATE SET geom = EXCLUDED.geom";

    public GisFileLoader() {
        super(log); // 부모 클래스에 로거 전달
    }

    // --- AbstractFileLoader 구현 ---

    @Override
    protected String getLoaderName() {
        return "GIS 파일 (ana_stats_data_mst)";
    }

    @Override
    protected Path getSourceDirectory() {
        return Paths.get(dssLoaderGisSrc);
    }

    @Override
    protected Path getProcessedDirectory() {
        return Paths.get(dssLoaderGisTmp);
    }

    @Override
    protected boolean isTargetFile(Path path) {
        String fileName = path.getFileName().toString().toLowerCase();
        return fileName.endsWith(".shp") ||
               fileName.endsWith(".geojson") ||
               fileName.endsWith(".gml");
    }

    @Override
    protected String getInsertSql() {
        return INSERT_SQL;
    }

    @Override
    protected int getChunkSize() {
        return CHUNK_SIZE;
    }

    /**
     * GIS 파일 그룹(e.g., .shp, .shx, .dbf)을 함께 이동시킵니다.
     */
    @Override
    protected void moveProcessedFileGroup(Path sourceFile, Path processedDir) throws IOException {
        String baseName = getFileBaseName(sourceFile);
        Path sourceDir = sourceFile.getParent();

        try (Stream<Path> relatedFiles = Files.list(sourceDir)) {
            relatedFiles
                    .filter(p -> p.getFileName().toString().startsWith(baseName + "."))
                    .forEach(p -> {
                        try {
                            Files.move(p,
                                    processedDir.resolve(p.getFileName()),
                                    StandardCopyOption.REPLACE_EXISTING);
                        } catch (IOException e) {
                            log.warn("관련 파일 이동 실패: {}", p.getFileName(), e);
                        }
                    });
        }
    }

    /**
     * GeoTools를 사용해 실제 GIS 파일을 파싱하고 WKT로 변환합니다.
     */
    @Override
    protected List<Object[]> parseFile(Path filePath) throws Exception {
        DataStore store = null;
        FeatureIterator<SimpleFeature> iterator = null;
        List<Object[]> fileData = new ArrayList<>();

        try {
            File file = filePath.toFile();
            Map<String, Serializable> params = new HashMap<>();
            params.put("url", file.toURI().toURL());

            if (file.getName().toLowerCase().endsWith(".shp")) {
                // Shapefile은 EUC-KR(MS949)일 수 있으나,
                // 최근 추세 및 호환성을 위해 UTF-8을 기본 시도.
                // .cpg 파일이 있다면 해당 인코딩을 따르는 것이 가장 좋음.
                // 여기서는 UTF-8을 기본으로 가정.
                params.put("charset", "UTF-8");
            }

            store = DataStoreFinder.getDataStore(params);
            if (store == null) {
                throw new IOException("파일에 맞는 DataStore를 찾지 못함: " + file.getName());
            }

            FeatureSource<SimpleFeatureType, SimpleFeature> source = store.getFeatureSource(store.getTypeNames()[0]);
            CoordinateReferenceSystem sourceCRS = source.getSchema().getCoordinateReferenceSystem();
            SimpleFeatureType schema = source.getSchema();

            // ID 필드 찾기
            String scpCdAttributeName = ID_FIELD_CANDIDATES.stream()
                    .filter(name -> schema.getDescriptor(name) != null)
                    .findFirst()
                    .orElseThrow(() -> new IOException("유효한 ID 속성을 찾지 못함: " + ID_FIELD_CANDIDATES));

            String scpTy = convertFieldNameToScpTy(scpCdAttributeName);

            FeatureCollection<SimpleFeatureType, SimpleFeature> collection = source.getFeatures();
            iterator = collection.features();

            while (iterator.hasNext()) {
                SimpleFeature feature = iterator.next();

                Object val = feature.getAttribute(scpCdAttributeName);
                String scpCd = (val != null) ? val.toString() : null;
                Geometry sourceGeom = (Geometry) feature.getDefaultGeometry();

                if (scpCd == null || scpCd.trim().isEmpty() || sourceGeom == null) {
                    log.warn("데이터 누락 (스킵): ID={}, Geom={}", scpCd, sourceGeom != null);
                    continue;
                }

                // MultiPolygon으로 통일
                MultiPolygon multiPolygon = convertToMultiPolygon(sourceGeom);
                multiPolygon.setSRID(SOURCE_SRID);

                String wktGeom = wktWriter.write(multiPolygon);
                fileData.add(new Object[]{ scpCd, scpTy, wktGeom });
            }

            return fileData;

        } finally {
            if (iterator != null) iterator.close();
            if (store != null) store.dispose();
        }
    }

    // --- GisFileLoader 고유 헬퍼 메서드 ---

    private String getFileBaseName(Path path) {
        String fileName = path.getFileName().toString();
        int dotIndex = fileName.lastIndexOf('.');
        return (dotIndex == -1) ? fileName : fileName.substring(0, dotIndex);
    }

    private static String convertFieldNameToScpTy(String fieldName) {
        return switch (fieldName) {
            case "TOT_REG_CD" -> "R";
            case "GRID_1K_CD" -> "K";
            case "GRID_100M_" -> "M";
            default -> null; // 로직상 scpCdAttributeName이 후보군 중 하나이므로 null이 될 수 없음
        };
    }

    private MultiPolygon convertToMultiPolygon(Geometry geom) {
        if (geom instanceof MultiPolygon) {
            return (MultiPolygon) geom;
        } else if (geom instanceof Polygon) {
            return geometryFactory.createMultiPolygon(new Polygon[] { (Polygon) geom });
        } else {
            // 기타 타입(Point, LineString 등)이 들어올 경우 경고 후 빈 MultiPolygon 반환
            log.warn("예상치 못한 지오메트리 타입(MultiPolygon으로 변환): {}", geom.getGeometryType());
            // 빈 MultiPolygon을 생성하거나, 예외를 발생시킬 수 있음.
            // 여기서는 빈 데이터를 생성하여 null 대신 반환.
            return geometryFactory.createMultiPolygon(null);
        }
    }
}