package incheon.cmm.g2f.download.service.Impl;

import java.util.function.Consumer;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

import incheon.cmm.g2f.download.service.G2FDownloadService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import incheon.cmm.g2f.download.mapper.DownloadMapper;
import incheon.cmm.g2f.download.repository.DownloadDataRepository;
import incheon.cmm.g2f.download.service.DownloadUtilService;
import incheon.cmm.g2f.layer.vo.TaskLayerVO;
import incheon.cmm.g2f.layer.vo.userLayerSearchRequestDTO;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
public class G2FDownloadServiceImpl implements G2FDownloadService {

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

	private final DownloadMapper downloadMapper;
	private final DownloadDataRepository downloadDataRepository;
	private final DownloadUtilService downloadUtilService;
	String timeSet = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd:HHmmss"));

	@Autowired
	public G2FDownloadServiceImpl(DownloadMapper downloadMapper, DownloadDataRepository downloadDataRepository,
			DownloadUtilService downloadUtilService) {
		this.downloadMapper = downloadMapper;
		this.downloadDataRepository = downloadDataRepository;
		this.downloadUtilService = downloadUtilService;
	}

	@Override
	public ResponseEntity<Resource> download(Long layerId, String layerType, String format, String columns)
			throws IOException {

		Path tempDir = null;

		try {
			// 1~4. 레이어 정보 조회 및 메타 처리 (기존과 동일)
			Map<String, Object> layerInfo;
			if ("TASK".equalsIgnoreCase(layerType)) {
				layerInfo = downloadMapper.selectDownloadLayerInfo(layerId);
			} else if ("USER".equalsIgnoreCase(layerType)) {
				layerInfo = downloadMapper.selectDownloadUserLayerInfo(layerId);
			} else {
				throw new IllegalArgumentException("알 수 없는 layerType: " + layerType);
			}

			if (layerInfo == null) {
				throw new RuntimeException("레이어 정보를 찾을 수 없습니다. ID: " + layerId);
			}

			String rawName;
			if ("TASK".equalsIgnoreCase(layerType)) {
				rawName = (String) layerInfo.get("tasklyrnm");
			} else {
				rawName = (String) layerInfo.get("user_lyr_nm");
			}
			if (rawName == null || rawName.isBlank())
				rawName = "download";
			rawName = rawName.trim();
			String baseName = sanitizeFileBaseName(rawName);

			String tableName;
			String schemaName;
			String tableNm;
			Integer srid;
			String spceTy;

			if ("TASK".equalsIgnoreCase(layerType)) {
				tableName = (String) layerInfo.get("lyrphysnm");
				schemaName = tableName.split("\\.")[0];
				tableNm = (String) layerInfo.get("lyrsrvcnm");
				srid = (Integer) layerInfo.get("cntm");
				spceTy = (String) layerInfo.get("spcety");
			} else {
				tableName = (String) layerInfo.get("lyr_phys_nm");
				schemaName = tableName.split("\\.")[0];
				tableNm = (String) layerInfo.get("lyr_srvc_nm");
				srid = (Integer) layerInfo.get("cntm");
				spceTy = (String) layerInfo.get("spce_ty");
			}

			if (tableName == null || tableName.isBlank()) {
				throw new RuntimeException("유효하지 않은 테이블명입니다.");
			}

			String geomColTmp = downloadDataRepository.selectGeometryColumnName(schemaName, tableNm);
			if (geomColTmp == null || geomColTmp.isBlank()) {
				geomColTmp = "geom";
			}
			final String finalGeomColumn = geomColTmp;

			/*
			 * ===================================================== 5. EXCEL / CSV :
			 * streaming (기존 유지) =====================================================
			 */
			if ("excel".equalsIgnoreCase(format) || "csv".equalsIgnoreCase(format)) {
				// 기존 코드 그대로 유지
				final boolean isExcel = "excel".equalsIgnoreCase(format);
				final Path finalTempDir = downloadUtilService.createDownloadTempDirectory();
				tempDir = finalTempDir;

				final int MAX_ROWS = 100_000;
				List<Map<String, Object>> buffer = new ArrayList<>(MAX_ROWS);
				List<Path> createdFiles = new ArrayList<>();
				AtomicInteger fileIndex = new AtomicInteger(1);

				downloadDataRepository.streamExcelDownloadData(tableName, finalGeomColumn, row -> {
					buffer.add(row);
					row.remove(finalGeomColumn);
					row.remove("wkb_geometry");
					row.remove("wkt_geometry");
					row.remove("geometry_srid");
					row.remove("gid");

					if (buffer.size() == MAX_ROWS) {
						try {
							Path p = isExcel ? downloadUtilService.createExcelFile(finalTempDir,
									baseName + "_" + fileIndex.getAndIncrement(), new ArrayList<>(buffer), columns)
									: downloadUtilService.createCsvFile(finalTempDir,
											baseName + "_" + fileIndex.getAndIncrement(), new ArrayList<>(buffer),
											columns);
							createdFiles.add(p);
							buffer.clear();
						} catch (IOException e) {
							throw new RuntimeException(e);
						}
					}
				});

				if (!buffer.isEmpty()) {
					Path p = isExcel
							? downloadUtilService.createExcelFile(finalTempDir,
									baseName + "_" + fileIndex.getAndIncrement(), buffer, columns)
							: downloadUtilService.createCsvFile(finalTempDir,
									baseName + "_" + fileIndex.getAndIncrement(), buffer, columns);
					createdFiles.add(p);
				}

				Path filePath;
				String downloadFileName;

				if (createdFiles.size() == 1) {
					filePath = createdFiles.get(0);
					downloadFileName = rawName + timeSet + (isExcel ? ".xlsx" : ".csv");
				} else {
					filePath = finalTempDir.resolve(baseName + "_split.zip");
					try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(filePath))) {
						for (Path p : createdFiles) {
							zos.putNextEntry(new ZipEntry(p.getFileName().toString()));
							Files.copy(p, zos);
							zos.closeEntry();
						}
					}
					downloadFileName = rawName + ".zip";
				}

				ResponseEntity<Resource> response = downloadUtilService.createFileDownloadResponse(filePath,
						downloadFileName);

				scheduleDelayedCleanup(finalTempDir, 10_000);
				return response;
			}

			/*
			 * ===================================================== 6. Shapefile, DXF,
			 * GeoJSON : Streaming 방식 ⭐ List 방식 코드 완전 삭제
			 * =====================================================
			 */
			tempDir = downloadUtilService.createDownloadTempDirectory();

			Path filePath;
			String downloadFileName;

			switch (format.toLowerCase()) {
			case "shapefile":
				List<Map<String, Object>> shapeData = downloadDataRepository.selectShapefileDownloadData(tableName,
						finalGeomColumn);

				filePath = downloadUtilService.createShapefileFromPostGIS(tempDir, tableName, baseName, srid, spceTy,
						shapeData, // List로 직접 전달
						finalGeomColumn);
				downloadFileName = rawName + timeSet + ".zip";
				break;

			case "dxf":
				filePath = downloadUtilService.createDxfFile(tempDir, baseName, (rowConsumer) -> {
					downloadDataRepository.streamShapefileDxfData(tableName, finalGeomColumn, rowConsumer);
				}, columns);
				downloadFileName = rawName + timeSet + ".dxf";
				break;

			case "geojson":
				Path geoJsonPath = tempDir.resolve(baseName + ".geojson");

				try (BufferedWriter writer = Files.newBufferedWriter(geoJsonPath)) {
					writer.write("{\"type\":\"FeatureCollection\",\"features\":[");
					writer.newLine();

					AtomicBoolean first = new AtomicBoolean(true);

					downloadDataRepository.streamGeoJsonFeature(tableName, finalGeomColumn, featureJson -> {
						try {
							if (!first.getAndSet(false)) {
								writer.write(",");
								writer.newLine();
							}
							writer.write(featureJson);
						} catch (IOException e) {
							throw new RuntimeException(e);
						}
					});

					writer.newLine();
					writer.write("]}");
				}

				filePath = geoJsonPath;
				downloadFileName = rawName + timeSet + ".geojson";
				break;

			default:
				throw new IllegalArgumentException("지원하지 않는 포맷: " + format);
			}

			ResponseEntity<Resource> response = downloadUtilService.createFileDownloadResponse(filePath,
					downloadFileName);

			scheduleDelayedCleanup(tempDir, 10_000);
			return response;

		} catch (IOException e) {
			if (tempDir != null) {
				downloadUtilService.cleanupDownloadFiles(tempDir);
			}
			throw e;
		}
	}

	@Override
	public Map<String, Object> getDownloadInfo(Long layerId) {
		return downloadMapper.selectDownloadLayerInfo(layerId);
	}

	@Override
	public Map<String, Object> getUserDownloadInfo(Long layerId) {
		return downloadMapper.selectDownloadUserLayerInfo(layerId);
	}

	private void scheduleDelayedCleanup(Path tempDir, long delayMillis) {
		if (tempDir == null)
			return;

		final Path dir = tempDir;
		new Thread(() -> {
			try {
				Thread.sleep(delayMillis);
				downloadUtilService.cleanupDownloadFiles(dir);
			} catch (InterruptedException ignored) {
				logger.warn(ignored.getMessage(), ignored);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}, "Download-Cleanup-" + System.currentTimeMillis()).start();
	}

	@Override
	public List<String> getSupportedFormats(Long layerId) {
		return List.of("shapefile", "csv", "excel", "dxf", "geojson");
	}

	@Override
	public List<TaskLayerVO> selectUserLayerListTot(userLayerSearchRequestDTO searchVO) {
		return downloadMapper.selectUserLayerListTot(searchVO);
	}

	@Override
	public long selectUserLayerListTotCnt(userLayerSearchRequestDTO searchVO) {
		return downloadMapper.selectUserLayerListTotCnt(searchVO);
	}

	/**
	 * 로컬 파일명에서 위험한 문자 치환
	 */
	private String sanitizeFileBaseName(String name) {
		if (name == null || name.isBlank())
			return "download";

		// 1) 슬래시 특별처리 (경로 구분자로 OS에서 절대 불가)
		name = name.replace("/", "-");

		// 2) 나머지 위험 문자만 치환
		String sanitized = name.replaceAll("[\\\\:*?\"<>|]", "_").trim();

		if (sanitized.isEmpty())
			return "download";
		return sanitized;
	}

}
