package com.kamco.cd.kamcoback.inference.service;

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 com.kamco.cd.kamcoback.inference.dto.InferenceResultShpDto;
import com.kamco.cd.kamcoback.inference.dto.WriteCnt;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.data.SimpleFeatureStore;
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.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.geojson.geom.GeometryJSON;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Geometry;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class GeoToolsShpWriter implements ShpWriter {

  // DBF 파일 한글 깨짐 방지를 위해 EUC-KR 사용
  private static final Charset DBF_CHARSET = Charset.forName("EUC-KR");

  // GeoJSON 출력은 UTF-8
  private static final Charset GEOJSON_CHARSET = StandardCharsets.UTF_8;

  // 좌표계: Korea 2000 / Central Belt 2010
  private static final String EPSG_5186 = "EPSG:5186";

  /**
   * SHP 파일(.shp/.shx/.dbf/.prj)을 생성한다.
   *
   * <p>- shpBasePath를 기준으로 파일을 생성한다. 예) /Users/kim/export/shp/1_map_2021_2022 → 1_map_2021_2022.shp
   * → 1_map_2021_2022.shx → 1_map_2021_2022.dbf → 1_map_2021_2022.prj
   *
   * <p>- geometry 타입은 첫 번째 유효 geometry 기준으로 스키마를 생성한다. - 좌표계는 EPSG:5186으로 설정하며, .prj 파일을 직접 생성한다.
   *
   * @param shpBasePath 확장자를 제외한 SHP 파일 기본 경로
   * @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
   * @return 이번 호출로 write(생성/덮어쓰기)가 수행된 파일 개수
   */
  @Override
  public WriteCnt writeShp(String shpBasePath, List<InferenceResultShpDto.Basic> rows) {

    if (rows == null || rows.isEmpty()) {
      return WriteCnt.zero();
    }

    // SHP는 Geometry.class를 허용하지 않으므로
    // 첫 번째 유효 geometry의 "구체 타입"을 기준으로 스키마를 생성한다.
    Geometry firstGeom = firstNonNullGeometry(rows);
    if (firstGeom == null) {
      throw new IllegalArgumentException("SHP 생성 실패: geometry가 전부 null 입니다. path=" + shpBasePath);
    }

    @SuppressWarnings("unchecked")
    Class<? extends Geometry> geomType = (Class<? extends Geometry>) firstGeom.getClass();

    ShapefileDataStore dataStore = null;

    try {
      File shpFile = new File(shpBasePath + ".shp");
      createDirectories(shpFile);

      // EPSG:5186 CRS 로딩
      CoordinateReferenceSystem crs = CRS.decode(EPSG_5186, false);

      // FeatureType(schema) 생성
      SimpleFeatureType schema = createSchema(geomType, crs);

      // ShapefileDataStore 생성 (기존 파일이 있어도 새로 생성/overwrite 동작)
      dataStore = createDataStore(shpFile, schema);

      // FeatureCollection 생성
      DefaultFeatureCollection collection = buildFeatureCollection(schema, rows);

      // 실제 SHP 파일에 feature 쓰기
      writeFeatures(dataStore, collection);

      // .prj 파일 직접 생성 (EPSG:5186)
      writePrjFile(shpBasePath, crs);

      log.info("SHP 생성 완료: {} ({} features)", shpFile.getAbsolutePath(), collection.size());

      // 덮어쓰기 포함: 이번 호출이 정상 종료되면 4개 파일 write가 발생했다고 카운트
      return new WriteCnt(1, 1, 1, 1, 0);

    } catch (Exception e) {
      throw new RuntimeException("SHP 생성 실패: " + shpBasePath, e);
    } finally {
      if (dataStore != null) {
        try {
          dataStore.dispose();
        } catch (Exception ignore) {
        }
      }
    }
  }

  /**
   * GeoJSON 파일(.geojson)을 생성한다.
   *
   * <p>- FeatureCollection 형태로 출력한다. - 최상단에 name / crs / properties를 포함한다. - 각 Feature는 polygon 단위로
   * 생성된다. - geometry는 GeoTools GeometryJSON을 사용하여 직렬화한다.
   *
   * <p>GeoJSON 구조 예: { "type": "FeatureCollection", "name": "stage_input1_input2_mapId", "crs": {
   * "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::5186" } }, "properties": { ...
   * }, "features": [ ... ] }
   *
   * @param geoJsonPath 생성할 GeoJSON 파일의 전체 경로 (.geojson 포함)
   * @param rows 동일 그룹(stage, mapId, input1, input2)의 데이터 목록
   * @return 이번 호출로 write(생성/덮어쓰기)가 수행된 파일 개수
   */
  @Override
  public WriteCnt writeGeoJson(String geoJsonPath, List<InferenceResultShpDto.Basic> rows) {

    if (rows == null || rows.isEmpty()) {
      return WriteCnt.zero();
    }

    try {
      File geoJsonFile = new File(geoJsonPath);
      createDirectories(geoJsonFile);

      // 그룹 공통 메타 정보는 첫 row 기준
      InferenceResultShpDto.Basic first = rows.get(0);

      ObjectMapper om = new ObjectMapper();
      GeometryJSON gj = new GeometryJSON(15);

      // FeatureCollection 루트
      ObjectNode root = om.createObjectNode();
      root.put("type", "FeatureCollection");

      // name: stage_input1_input2_mapId
      String name =
          String.format(
              "%d_%d_%d_%d",
              first.getStage(), first.getInput1(), first.getInput2(), first.getMapId());
      root.put("name", name);

      // CRS (EPSG:5186)
      ObjectNode crs = om.createObjectNode();
      crs.put("type", "name");
      ObjectNode crsProps = om.createObjectNode();
      crsProps.put("name", "urn:ogc:def:crs:EPSG::5186");
      crs.set("properties", crsProps);
      root.set("crs", crs);

      // 그룹 공통 properties
      ObjectNode groupProps = om.createObjectNode();
      groupProps.put("stage", first.getStage());
      groupProps.put("input1", first.getInput1());
      groupProps.put("input2", first.getInput2());
      groupProps.put("map_id", first.getMapId());
      root.set("properties", groupProps);

      // features 배열
      ArrayNode features = om.createArrayNode();

      for (InferenceResultShpDto.Basic dto : rows) {
        if (dto.getGeometry() == null) {
          continue;
        }

        ObjectNode feature = om.createObjectNode();
        feature.put("type", "Feature");

        // feature properties
        ObjectNode p = om.createObjectNode();
        p.put("polygon_id", dto.getUuid() != null ? dto.getUuid().toString() : null);
        if (dto.getCdProb() != null) {
          p.put("cd_prob", dto.getCdProb());
        }
        if (dto.getInput1() != null) {
          p.put("input1", dto.getInput1());
        }
        if (dto.getInput2() != null) {
          p.put("input2", dto.getInput2());
        }
        if (dto.getMapId() != null) {
          p.put("map_id", dto.getMapId());
        }
        if (dto.getArea() != null) {
          p.put("area", dto.getArea());
        }
        p.put("before_c", dto.getBeforeClass());
        if (dto.getBeforeProbability() != null) {
          p.put("before_p", dto.getBeforeProbability());
        }
        p.put("after_c", dto.getAfterClass());
        if (dto.getAfterProbability() != null) {
          p.put("after_p", dto.getAfterProbability());
        }

        feature.set("properties", p);

        // geometry
        String geomJson = gj.toString(dto.getGeometry());
        JsonNode geomNode = om.readTree(geomJson);
        feature.set("geometry", geomNode);

        features.add(feature);
      }

      root.set("features", features);

      // 파일 쓰기
      try (OutputStreamWriter w =
          new OutputStreamWriter(new FileOutputStream(geoJsonFile), GEOJSON_CHARSET)) {
        om.writerWithDefaultPrettyPrinter().writeValue(w, root);
      }

      log.info("GeoJSON 생성 완료: {} ({} features)", geoJsonFile.getAbsolutePath(), features.size());

      // 덮어쓰기 포함: 이번 호출이 정상 종료되면 geojson 1개 write로 카운트
      return new WriteCnt(0, 0, 0, 0, 1);

    } catch (Exception e) {
      throw new RuntimeException("GeoJSON 생성 실패: " + geoJsonPath, e);
    }
  }

  private Geometry firstNonNullGeometry(List<InferenceResultShpDto.Basic> rows) {
    for (InferenceResultShpDto.Basic r : rows) {
      if (r != null && r.getGeometry() != null) {
        return r.getGeometry();
      }
    }
    return null;
  }

  private SimpleFeatureType createSchema(
      Class<? extends Geometry> geomType, CoordinateReferenceSystem crs) {
    SimpleFeatureTypeBuilder b = new SimpleFeatureTypeBuilder();
    b.setName("inference_result");
    b.setCRS(crs);

    // geometry는 반드시 첫 컬럼
    b.add("the_geom", geomType);

    // DBF 컬럼 정의 (10자 제한 고려)
    b.length(36).add("poly_id", String.class);
    b.add("cd_prob", Double.class);
    b.add("input1", Integer.class);
    b.add("input2", Integer.class);
    b.add("map_id", Long.class);
    b.add("area", Double.class);
    b.length(20).add("before_c", String.class);
    b.add("before_p", Double.class);
    b.length(20).add("after_c", String.class);
    b.add("after_p", Double.class);

    return b.buildFeatureType();
  }

  /**
   * .shp .shx .dbf .fix 파일 생성 (껍데기 생성)
   *
   * @param shpFile
   * @param schema
   * @return
   * @throws Exception
   */
  private ShapefileDataStore createDataStore(File shpFile, SimpleFeatureType schema)
      throws Exception {

    Map<String, Serializable> params = new HashMap<>();
    params.put("url", shpFile.toURI().toURL());

    // .fix 파일 생성 Boolean.TRUE, 미생성 Boolean.FALSE
    params.put("create spatial index", Boolean.FALSE);

    ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory();

    ShapefileDataStore dataStore = (ShapefileDataStore) factory.createNewDataStore(params);

    dataStore.setCharset(DBF_CHARSET);
    dataStore.createSchema(schema);

    return dataStore;
  }

  private DefaultFeatureCollection buildFeatureCollection(
      SimpleFeatureType schema, List<InferenceResultShpDto.Basic> rows) {
    DefaultFeatureCollection collection = new DefaultFeatureCollection();
    SimpleFeatureBuilder builder = new SimpleFeatureBuilder(schema);

    for (InferenceResultShpDto.Basic dto : rows) {
      if (dto == null || dto.getGeometry() == null) {
        continue;
      }

      builder.add(dto.getGeometry());
      builder.add(dto.getUuid() != null ? dto.getUuid().toString() : null);
      builder.add(dto.getCdProb() != null ? dto.getCdProb().doubleValue() : null);
      builder.add(dto.getInput1());
      builder.add(dto.getInput2());
      builder.add(dto.getMapId());
      builder.add(dto.getArea() != null ? dto.getArea().doubleValue() : null);
      builder.add(dto.getBeforeClass());
      builder.add(
          dto.getBeforeProbability() != null ? dto.getBeforeProbability().doubleValue() : null);
      builder.add(dto.getAfterClass());
      builder.add(
          dto.getAfterProbability() != null ? dto.getAfterProbability().doubleValue() : null);

      SimpleFeature feature = builder.buildFeature(null);
      collection.add(feature);
      builder.reset();
    }

    return collection;
  }

  private void writeFeatures(ShapefileDataStore dataStore, DefaultFeatureCollection collection)
      throws Exception {

    String typeName = dataStore.getTypeNames()[0];
    SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);

    if (!(featureSource instanceof SimpleFeatureStore store)) {
      throw new IllegalStateException("FeatureStore 생성 실패");
    }

    store.setTransaction(Transaction.AUTO_COMMIT);
    store.addFeatures(collection);
    store.getTransaction().commit();
  }

  private void writePrjFile(String shpBasePath, CoordinateReferenceSystem crs) throws Exception {

    File prjFile = new File(shpBasePath + ".prj");
    createDirectories(prjFile);

    Files.writeString(prjFile.toPath(), crs.toWKT(), StandardCharsets.UTF_8);
  }

  private void createDirectories(File file) throws Exception {
    File parent = file.getParentFile();
    if (parent != null) {
      Files.createDirectories(parent.toPath());
    }
  }
}
