package incheon.product.geoview2d.download.service.impl;

import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import incheon.product.geoview2d.download.service.FileFormatConverter;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.geotools.api.data.SimpleFeatureStore;
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.collection.ListFeatureCollection;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.referencing.CRS;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.io.WKTReader;
import org.springframework.stereotype.Service;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 파일 포맷 변환 서비스 구현체.
 */
@Slf4j
@Service("productFileFormatConverter")
public class FileFormatConverterImpl extends EgovAbstractServiceImpl implements FileFormatConverter {

    private static final Map<String, Class<? extends Geometry>> GEOM_TYPE_MAP = Map.of(
            "POINT", Point.class,
            "LINESTRING", LineString.class,
            "POLYGON", Polygon.class,
            "MULTIPOINT", MultiPoint.class,
            "MULTILINESTRING", MultiLineString.class,
            "MULTIPOLYGON", MultiPolygon.class
    );

    @Override
    public Path createShapefile(Path directory, String fileName, List<Map<String, Object>> data,
                                List<String> columns, String spceTy, int srid) throws IOException {
        Path zipFile = directory.resolve(sanitizeFileName(fileName) + ".zip");

        try {
            CoordinateReferenceSystem crs = CRS.decode("EPSG:" + srid, true);
            Class<? extends Geometry> geomClass = GEOM_TYPE_MAP.getOrDefault(
                    spceTy != null ? spceTy.toUpperCase() : "MULTIPOLYGON", MultiPolygon.class);

            SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder();
            typeBuilder.setName("layer");
            typeBuilder.setCRS(crs);
            typeBuilder.add("the_geom", geomClass);
            for (String col : columns) {
                typeBuilder.add(col, String.class);
            }
            SimpleFeatureType featureType = typeBuilder.buildFeatureType();

            Path shpFile = directory.resolve(sanitizeFileName(fileName) + ".shp");
            ShapefileDataStoreFactory factory = new ShapefileDataStoreFactory();
            ShapefileDataStore store = (ShapefileDataStore) factory.createNewDataStore(
                    Map.of("url", shpFile.toUri().toURL()));
            try {
                store.createSchema(featureType);
                store.setCharset(StandardCharsets.UTF_8);

                WKTReader wktReader = new WKTReader();
                SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType);
                List<SimpleFeature> features = new ArrayList<>();

                for (Map<String, Object> row : data) {
                    String wkt = (String) row.get("wkt_geometry");
                    if (wkt != null) {
                        Geometry geom = wktReader.read(wkt);
                        featureBuilder.add(geom);
                    }
                    for (String col : columns) {
                        Object val = row.get(col);
                        featureBuilder.add(val != null ? val.toString() : "");
                    }
                    features.add(featureBuilder.buildFeature(null));
                }

                SimpleFeatureStore featureStore = (SimpleFeatureStore) store.getFeatureSource(store.getTypeNames()[0]);
                DefaultTransaction transaction = new DefaultTransaction("create");
                featureStore.setTransaction(transaction);
                try {
                    featureStore.addFeatures(new ListFeatureCollection(featureType, features));
                    transaction.commit();
                } catch (Exception e) {
                    try {
                        transaction.rollback();
                    } catch (Exception rollbackEx) {
                        e.addSuppressed(rollbackEx);
                    }
                    throw e;
                } finally {
                    transaction.close();
                }
            } finally {
                store.dispose();
            }

            // ZIP
            try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipFile))) {
                String baseName = sanitizeFileName(fileName);
                for (String ext : new String[]{".shp", ".shx", ".dbf", ".prj", ".cpg"}) {
                    Path file = directory.resolve(baseName + ext);
                    if (Files.exists(file)) {
                        zos.putNextEntry(new ZipEntry(baseName + ext));
                        Files.copy(file, zos);
                        zos.closeEntry();
                    }
                }
            }

        } catch (Exception e) {
            throw new IOException("Shapefile 생성 실패", e);
        }

        return zipFile;
    }

    @Override
    public Path createCsvFile(Path directory, String fileName, List<String> columns,
                              Consumer<Consumer<Map<String, Object>>> dataProvider) throws IOException {
        Path csvFile = directory.resolve(sanitizeFileName(fileName) + ".csv");

        try (BufferedWriter writer = new BufferedWriter(
                new OutputStreamWriter(Files.newOutputStream(csvFile), StandardCharsets.UTF_8))) {
            // BOM
            writer.write('\uFEFF');
            // Header
            writer.write(String.join(",", columns));
            writer.newLine();

            dataProvider.accept(row -> {
                try {
                    StringBuilder sb = new StringBuilder();
                    for (int i = 0; i < columns.size(); i++) {
                        if (i > 0) sb.append(",");
                        Object val = row.get(columns.get(i));
                        String str = val != null ? val.toString() : "";
                        if (str.contains(",") || str.contains("\"") || str.contains("\n")) {
                            sb.append("\"").append(str.replace("\"", "\"\"")).append("\"");
                        } else {
                            sb.append(str);
                        }
                    }
                    writer.write(sb.toString());
                    writer.newLine();
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });
        }

        return csvFile;
    }

    @Override
    public Path createExcelFile(Path directory, String fileName, List<String> columns,
                                Consumer<Consumer<Map<String, Object>>> dataProvider) throws IOException {
        Path excelFile = directory.resolve(sanitizeFileName(fileName) + ".xlsx");

        try (SXSSFWorkbook workbook = new SXSSFWorkbook(200)) {
            Sheet sheet = workbook.createSheet(sanitizeFileName(fileName));

            // Header
            Row headerRow = sheet.createRow(0);
            for (int i = 0; i < columns.size(); i++) {
                headerRow.createCell(i).setCellValue(columns.get(i));
                sheet.setColumnWidth(i, 6000);
            }

            int[] rowIndex = {1};
            dataProvider.accept(row -> {
                Row dataRow = sheet.createRow(rowIndex[0]++);
                for (int i = 0; i < columns.size(); i++) {
                    Cell cell = dataRow.createCell(i);
                    Object val = row.get(columns.get(i));
                    if (val == null) {
                        cell.setCellValue("");
                    } else if (val instanceof Number) {
                        cell.setCellValue(((Number) val).doubleValue());
                    } else if (val instanceof Boolean) {
                        cell.setCellValue((Boolean) val);
                    } else {
                        cell.setCellValue(val.toString());
                    }
                }
            });

            try (OutputStream os = Files.newOutputStream(excelFile)) {
                workbook.write(os);
            }
        }

        return excelFile;
    }

    @Override
    public Path createDxfFile(Path directory, String fileName, List<String> columns,
                              Consumer<Consumer<Map<String, Object>>> dataProvider) throws IOException {
        Path dxfFile = directory.resolve(sanitizeFileName(fileName) + ".dxf");

        try (BufferedWriter writer = new BufferedWriter(
                new OutputStreamWriter(Files.newOutputStream(dxfFile), StandardCharsets.UTF_8), 102400)) {
            // DXF Header
            writeDxfHeader(writer);
            writeDxfTablesSection(writer);
            writeDxfBlocksSection(writer);

            // ENTITIES
            writer.write("0\nSECTION\n2\nENTITIES\n");
            dataProvider.accept(row -> {
                try {
                    String wkt = (String) row.get("wkt_geometry");
                    if (wkt != null) {
                        writeDxfEntity(writer, wkt, row, columns);
                    }
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });
            writer.write("0\nENDSEC\n0\nEOF\n");
        }

        return dxfFile;
    }

    @Override
    public Path createGeoJsonFile(Path directory, String fileName,
                                  Consumer<Consumer<String>> featureProvider) throws IOException {
        Path jsonFile = directory.resolve(sanitizeFileName(fileName) + ".geojson");

        try (BufferedWriter writer = new BufferedWriter(
                new OutputStreamWriter(Files.newOutputStream(jsonFile), StandardCharsets.UTF_8))) {
            writer.write("{\"type\":\"FeatureCollection\",\"features\":[");

            boolean[] first = {true};
            featureProvider.accept(feature -> {
                try {
                    if (!first[0]) writer.write(",");
                    writer.write(feature);
                    first[0] = false;
                } catch (IOException e) {
                    throw new UncheckedIOException(e);
                }
            });

            writer.write("]}");
        }

        return jsonFile;
    }

    @Override
    public String sanitizeFileName(String name) {
        if (name == null) return "download";
        return name.replaceAll("[\\\\/:*?\"<>|\\s]+", "_");
    }

    // ========== DXF helpers ==========

    private void writeDxfHeader(BufferedWriter writer) throws IOException {
        writer.write("0\nSECTION\n2\nHEADER\n");
        writer.write("9\n$ACADVER\n1\nAC1015\n");
        writer.write("0\nENDSEC\n");
    }

    private void writeDxfTablesSection(BufferedWriter writer) throws IOException {
        writer.write("0\nSECTION\n2\nTABLES\n");
        writer.write("0\nTABLE\n2\nLAYER\n70\n1\n");
        writer.write("0\nLAYER\n2\n0\n70\n0\n62\n7\n6\nContinuous\n");
        writer.write("0\nENDTAB\n");
        writer.write("0\nENDSEC\n");
    }

    private void writeDxfBlocksSection(BufferedWriter writer) throws IOException {
        writer.write("0\nSECTION\n2\nBLOCKS\n0\nENDSEC\n");
    }

    private void writeDxfEntity(BufferedWriter writer, String wkt, Map<String, Object> row, List<String> columns) throws IOException {
        String upper = wkt.toUpperCase().trim();
        if (upper.startsWith("POINT")) {
            writeDxfPoint(writer, wkt);
        } else if (upper.startsWith("LINESTRING") || upper.startsWith("POLYGON")) {
            writeDxfPolyline(writer, wkt);
        } else if (upper.startsWith("MULTI")) {
            // Simplified: write as single entity for now
            writeDxfPolyline(writer, wkt);
        }
    }

    private void writeDxfPoint(BufferedWriter writer, String wkt) throws IOException {
        String coords = wkt.replaceAll("(?i)POINT\\s*\\(", "").replace(")", "").trim();
        String[] parts = coords.split("\\s+");
        if (parts.length >= 2) {
            writer.write("0\nPOINT\n8\n0\n");
            writer.write("10\n" + parts[0] + "\n20\n" + parts[1] + "\n30\n0.0\n");
        }
    }

    private void writeDxfPolyline(BufferedWriter writer, String wkt) throws IOException {
        String coords = wkt.replaceAll("(?i)(MULTI)?(POLYGON|LINESTRING|POINT)\\s*\\(+", "").replaceAll("\\)+", "").trim();
        String[] points = coords.split(",");

        writer.write("0\nPOLYLINE\n8\n0\n66\n1\n70\n0\n");
        for (String point : points) {
            String[] parts = point.trim().split("\\s+");
            if (parts.length >= 2) {
                writer.write("0\nVERTEX\n8\n0\n");
                writer.write("10\n" + parts[0] + "\n20\n" + parts[1] + "\n30\n0.0\n");
            }
        }
        writer.write("0\nSEQEND\n8\n0\n");
    }
}
