package com.caliverse.admin.domain.service;

import com.caliverse.admin.domain.entity.excel.ExcelBusinessLog;
import com.caliverse.admin.global.component.tracker.ExcelProgressTracker;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Slf4j
@Service
@RequiredArgsConstructor
public class ExcelService {
    private final ExcelProgressTracker progressTracker;

    // CSV 구분자
    private static final String CSV_DELIMITER = ",";
    private static final String CSV_QUOTE = "\"";
    private static final String CSV_LINE_SEPARATOR = "\n";

    private static final Set<String> EXCLUDED_HEADERS = Set.of(
            "header.TranId",
            "header.Actor.AccountId",
            "header.Actor.AccountIdString",
            "header.Actor.UserGuid",
            "header.Actor.UserNickname",
            "header.Actor.CharacterMetaId",
            "header.Actor.UserLevel",
            "body.Action",
            "body.Infos[0].Domain",
            "statisticsType"
    );

    // 멀티 스레드 처리를 위한 스레드 풀
    private final ExecutorService executorService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
    );

    // 메타데이터 캐시 (리플렉션 비용 절약)
    private final Map<Class<?>, List<Field>> fieldCache = new ConcurrentHashMap<>();
    private final Map<String, String> translationCache = new ConcurrentHashMap<>();

    /**
     * Object 리스트의 첫 번째 객체 필드를 기준으로 자동 헤더 생성하여 엑셀 파일을 HttpServletResponse로 직접 출력
     * @param response HttpServletResponse 객체
     * @param data 변환할 데이터 객체 리스트
     * @param fileName 다운로드될 파일명
     * @param sheetName 시트 이름
     */
    public void generateExcelToResponse(HttpServletResponse response, List<?> data,
                                                       String fileName, String sheetName, String taskId) throws IOException {

        if (data == null || data.isEmpty()) {
            throw new IllegalArgumentException("데이터가 비어있습니다.");
        }

        int dataSize = data.size();
        log.info("Processing {} records for Excel download", dataSize);

        if (dataSize <= 50000) {
            // 5만건 이하: 단일 CSV 파일
            log.info("Creating single Excel file");
//            generateStreamingExcel(response, data, fileName, sheetName, taskId);
            generateStreamingCsv(response, data, fileName, taskId);
        } else if (dataSize <= 500000) {
            // 5만건 초과 ~ 50만건: 5만건씩 분할하여 ZIP 파일
            log.info("Creating multiple Excel files (split by 10,000 records)");
//            generateMultipleExcelFilesAsZipParallel(response, data, fileName, sheetName, 10000, taskId);
            generateMultipleCsvFilesAsZip(response, data, fileName, 50000, taskId);
        } else {
            // 50만건 초과: 에러
            throw new IllegalArgumentException(
                    String.format("데이터가 너무 많습니다. 최대 500,000건까지만 지원합니다. (현재: %,d건)", dataSize));
        }
    }

    /**
     * 메인 CSV 생성 메서드
     * @param response HttpServletResponse 객체
     * @param data 변환할 데이터 객체 리스트
     * @param fileName 다운로드될 파일명
     * @param taskId 진행률 추적 ID
     */
    public void generateCsvToResponse(HttpServletResponse response, List<?> data,
                                      String fileName, String taskId) throws IOException {

        if (data == null || data.isEmpty()) {
            throw new IllegalArgumentException("데이터가 비어있습니다.");
        }

        int dataSize = data.size();
        log.info("Processing {} records for CSV download", dataSize);

        if (dataSize <= 50000) {
            // 5만건 이하: 단일 CSV 파일
            log.info("Creating single CSV file");
            generateStreamingCsv(response, data, fileName, taskId);
        } else if (dataSize <= 500000) {
            // 5만건 초과 ~ 50만건: 5만건씩 분할하여 ZIP 파일
            log.info("Creating multiple CSV files (split by 50,000 records)");
            generateMultipleCsvFilesAsZip(response, data, fileName, 50000, taskId);
        } else {
            // 50만건 초과: 에러
            throw new IllegalArgumentException(
                    String.format("데이터가 너무 많습니다. 최대 500,000건까지만 지원합니다. (현재: %,d건)", dataSize));
        }
    }

    /**
     * 스트리밍 방식으로 CSV 생성 (메모리 효율적)
     */
    private void generateStreamingCsv(HttpServletResponse response, List<?> data,
                                      String fileName, String taskId) throws IOException {

        setupCsvResponseHeaders(response, fileName);
        progressTracker.updateProgress(taskId, 35, 100, "헤더 정보 생성중...");

        // 헤더 생성
        Map<String, String> headers = generateOptimizedHeaders(data);
        String[] fieldNames = headers.keySet().toArray(new String[0]);

        progressTracker.updateProgress(taskId, 40, 100, "CSV 파일 생성 시작...");

        try (OutputStreamWriter writer = new OutputStreamWriter(
                response.getOutputStream(), StandardCharsets.UTF_8);
             BufferedWriter bufferedWriter = new BufferedWriter(writer, 8192)) {

            // UTF-8 BOM 추가 (Excel에서 한글 깨짐 방지)
            bufferedWriter.write('\ufeff');

            // 헤더 행 작성
            writeCsvHeader(bufferedWriter, headers.values());
            progressTracker.updateProgress(taskId, 42, 100, "헤더 작성 완료. 데이터 처리 시작...");

            // 배치 처리로 데이터 작성
            int batchSize = 10000;
            int totalDataSize = data.size();
            int processedDataSize = 0;

            for (int i = 0; i < data.size(); i += batchSize) {
                int endIdx = Math.min(i + batchSize, data.size());
                List<?> batch = data.subList(i, endIdx);

                try {
                    // 배치를 미리 변환
                    List<Map<String, Object>> flattenedBatch = preProcessBatch(batch);

                    // CSV 행 작성
                    for (Map<String, Object> flatMap : flattenedBatch) {
                        if (flatMap != null) {
                            writeCsvRow(bufferedWriter, flatMap, fieldNames);
                        }
                    }

                    processedDataSize += batch.size();

                    // 진행률 업데이트
                    int progress = 42 + (int) ((processedDataSize * 53.0) / totalDataSize);
                    progressTracker.updateProgress(taskId, progress, 100,
                            String.format("데이터 처리 중... (%,d/%,d)", processedDataSize, totalDataSize));

                    // 주기적으로 flush
                    if (processedDataSize % 50000 == 0) {
                        bufferedWriter.flush();
                    }

                } catch (Exception e) {
                    log.error("Batch processing failed at index {}: {}", i, e.getMessage(), e);
                    processedDataSize += batch.size(); // 오류 발생한 배치도 처리된 것으로 간주
                }
            }

            bufferedWriter.flush();
            progressTracker.updateProgress(taskId, 100, 100,
                    String.format("CSV 다운로드 완료! (총 %,d건)", totalDataSize));

            log.info("CSV generation completed successfully for taskId: {} ({} rows)",
                    taskId, totalDataSize);

        } catch (IOException e) {
            log.error("CSV generation failed for taskId: {}", taskId, e);
            progressTracker.updateProgress(taskId, -1, 100, "CSV 생성 실패: " + e.getMessage());
            throw e;
        } catch (Exception e) {
            log.error("Unexpected error during CSV generation for taskId: {}", taskId, e);
            progressTracker.updateProgress(taskId, -1, 100, "예상치 못한 오류 발생: " + e.getMessage());
            throw new IOException("CSV 생성 중 예상치 못한 오류가 발생했습니다: " + e.getMessage(), e);
        }
    }

    /**
     * 병렬 처리로 다중 CSV 파일 ZIP 생성
     */
    private void generateMultipleCsvFilesAsZip(HttpServletResponse response, List<?> data,
                                               String fileName, int maxRowsPerFile, String taskId) throws IOException {

        setupZipResponseHeaders(response, fileName + ".zip");
        progressTracker.updateProgress(taskId, 35, 100, "헤더 정보 생성중...");

        Map<String, String> headers = generateOptimizedHeaders(data);

        try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) {
            int totalFiles = (int) Math.ceil((double) data.size() / maxRowsPerFile);
            progressTracker.updateProgress(taskId, 40, 100,
                    String.format("ZIP 파일 생성 준비 중... (총 %d개 파일)", totalFiles));

            AtomicInteger completedFiles = new AtomicInteger(0);

            // 청크를 병렬로 처리
            List<CompletableFuture<CsvFileData>> futures = new ArrayList<>();

            for (int fileIndex = 0; fileIndex < totalFiles; fileIndex++) {
                int startIndex = fileIndex * maxRowsPerFile;
                int endIndex = Math.min(startIndex + maxRowsPerFile, data.size());
                List<?> chunk = data.subList(startIndex, endIndex);

                final int currentFileIndex = fileIndex;
                CompletableFuture<CsvFileData> future = CompletableFuture.supplyAsync(() -> {
                    try {
                        String csvFileName = String.format("%s_part%d.csv",
                                fileName, currentFileIndex + 1);

                        byte[] csvData = createSingleCsvFileBytes(chunk, headers);

                        int completed = completedFiles.incrementAndGet();
                        progressTracker.updateProgress(taskId,
                                40 + (int) ((completed * 50.0) / totalFiles), 100,
                                String.format("CSV 파일 완료... (%d/%d)", completed, totalFiles));

                        return new CsvFileData(csvFileName, csvData, currentFileIndex);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }, executorService);

                futures.add(future);
            }

            // 완료된 순서대로 ZIP에 추가
            for (int i = 0; i < futures.size(); i++) {
                try {
                    CsvFileData fileData = futures.get(i).get();

                    ZipEntry zipEntry = new ZipEntry(fileData.fileName);
                    zipOut.putNextEntry(zipEntry);
                    zipOut.write(fileData.data);
                    zipOut.closeEntry();

                    int zipProgress = 90 + (int) (((i + 1) * 10.0) / totalFiles);
                    progressTracker.updateProgress(taskId, zipProgress, 100,
                            String.format("ZIP 압축 완료... (%d/%d)", i + 1, totalFiles));

                } catch (Exception e) {
                    log.error("Error processing file {}: {}", i, e.getMessage(), e);
                    throw new IOException("CSV file creation failed", e);
                }
            }

            // 인덱스 파일 생성
            createIndexFile(zipOut, fileName, totalFiles, data.size(), maxRowsPerFile);

            zipOut.finish();
            progressTracker.updateProgress(taskId, 100, 100, "ZIP 파일 생성 완료!");
        }
    }

    /**
     * CSV 헤더 작성
     */
    private void writeCsvHeader(BufferedWriter writer, Collection<String> headers) throws IOException {
        boolean first = true;
        for (String header : headers) {
            if (!first) {
                writer.write(CSV_DELIMITER);
            }
            writer.write(escapeCsvValue(header));
            first = false;
        }
        writer.write(CSV_LINE_SEPARATOR);
    }

    /**
     * CSV 데이터 행 작성
     */
    private void writeCsvRow(BufferedWriter writer, Map<String, Object> flatMap, String[] fieldNames) throws IOException {
        boolean first = true;
        for (String fieldName : fieldNames) {
            if (!first) {
                writer.write(CSV_DELIMITER);
            }
            Object value = flatMap.get(fieldName);
            writer.write(escapeCsvValue(value != null ? value.toString() : ""));
            first = false;
        }
        writer.write(CSV_LINE_SEPARATOR);
    }

    /**
     * CSV 값 이스케이프 처리
     */
    private String escapeCsvValue(String value) {
        if (value == null || value.isEmpty()) {
            return "";
        }

        // 쉼표, 개행, 큰따옴표가 포함된 경우 큰따옴표로 감싸기
        if (value.contains(CSV_DELIMITER) || value.contains(CSV_LINE_SEPARATOR) || value.contains(CSV_QUOTE)) {
            // 큰따옴표는 두 개로 이스케이프
            value = value.replace(CSV_QUOTE, CSV_QUOTE + CSV_QUOTE);
            return CSV_QUOTE + value + CSV_QUOTE;
        }

        return value;
    }

    /**
     * 단일 CSV 파일 바이트 생성
     */
    private byte[] createSingleCsvFileBytes(List<?> data, Map<String, String> headers) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8);
             BufferedWriter bufferedWriter = new BufferedWriter(writer)) {

            // UTF-8 BOM 추가
            bufferedWriter.write('\ufeff');

            // 헤더 작성
            writeCsvHeader(bufferedWriter, headers.values());

            // 데이터 작성
            String[] fieldNames = headers.keySet().toArray(new String[0]);
            List<Map<String, Object>> flattenedData = preProcessBatch(data);

            for (Map<String, Object> flatMap : flattenedData) {
                writeCsvRow(bufferedWriter, flatMap, fieldNames);
            }

            bufferedWriter.flush();
            return baos.toByteArray();
        }
    }


    /**
     * Object 리스트로부터 엑셀 파일을 HttpServletResponse로 직접 출력
     * @param response HttpServletResponse 객체
     * @param data 변환할 데이터 객체 리스트
     * @param headers 엑셀 헤더 (key: 필드명, value: 표시될 헤더명)
     * @param fileName 다운로드될 파일명
     * @param sheetName 시트 이름
     */
    public void generateExcelToResponse(HttpServletResponse response, List<?> data,
                                        Map<String, String> headers, String fileName, String sheetName) throws IOException {

        // 응답 헤더 설정
        setupResponseHeaders(response, fileName);

        // 엑셀 파일 생성 및 출력
        try (Workbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet(sheetName);

            // 헤더 스타일 설정
            CellStyle headerStyle = createHeaderStyle(workbook);

            // 헤더 행 생성
            createHeaderRow(sheet, headers, headerStyle);

            // 필드명 배열 생성 (순서 유지를 위해)
            String[] fieldNames = headers.keySet().toArray(new String[0]);

            // 데이터 행 생성
            createDataRows(sheet, data, fieldNames);

            // 칼럼 너비 자동 조정
            autoSizeColumns(sheet, headers.size());

            // 응답으로 엑셀 파일 출력
            workbook.write(response.getOutputStream());
        }
    }

    /**
     * 스트리밍 방식으로 엑셀 생성 (메모리 효율적)
     */
    private void generateStreamingExcel(HttpServletResponse response, List<?> data,
                                        String fileName, String sheetName, String taskId) throws IOException {
        XSSFWorkbook workbook = null;
        Sheet sheet = null;
        try {
            setupResponseHeaders(response, fileName);

            progressTracker.updateProgress(taskId, 35, 100, "헤더 정보 생성중...");
            // 헤더 미리 생성 (샘플링 최적화)
            Map<String, String> headers = generateOptimizedHeaders(data);
            String[] fieldNames = headers.keySet().toArray(new String[0]);
            progressTracker.updateProgress(taskId, 37, 100, "헤더 정보 완료...");

            try {
                workbook = new XSSFWorkbook();
                progressTracker.updateProgress(taskId, 38, 100, "workbook 생성 완료...");
                sheet = workbook.createSheet(sheetName);
                progressTracker.updateProgress(taskId, 39, 100, "sheet 생성 완료...");

                // 헤더 생성
                CellStyle headerStyle = createHeaderStyle(workbook);
                progressTracker.updateProgress(taskId, 39, 100, "헤더스타일 생성 완료...");
                createHeaderRow(sheet, headers, headerStyle);
                progressTracker.updateProgress(taskId, 40, 100, "헤더 행 생성완료. 데이터 처리 시작...");

            } catch (Exception e) {
                log.error("Workbook initialization failed for taskId: {}", taskId, e);
                progressTracker.updateProgress(taskId, -1, 100, "워크북 초기화 실패: " + e.getMessage());
                throw new IOException("Excel 워크북 초기화 중 오류가 발생했습니다: " + e.getMessage(), e);
            }

            // 배치 처리로 데이터 생성
            int batchSize = 5000;
            int rowIdx = 1;
            int totalDataSize = data.size();
            int processedDataSize = 0;
            int currentBatchNumber = 0;

            try {
                for (int i = 0; i < data.size(); i += batchSize) {
                    currentBatchNumber++;
                    int endIdx = Math.min(i + batchSize, data.size());
                    List<?> batch = data.subList(i, endIdx);

                    try {
                        // 배치를 미리 변환하여 성능 향상
                        List<Map<String, Object>> flattenedBatch = preProcessBatch(batch);

                        if (flattenedBatch == null || flattenedBatch.isEmpty()) {
                            log.warn("Batch {} is empty after preprocessing, skipping...", currentBatchNumber);
                            continue;
                        }

                        int conversionProgress = 40 + (int) (((processedDataSize + batch.size() * 0.3) * 50.0) / totalDataSize);
                        progressTracker.updateProgress(taskId, conversionProgress, 100,
                                String.format("엑셀 행 생성 중... (%,d/%,d)", processedDataSize, totalDataSize));

                        // 행 생성
                        for (int j = 0; j < flattenedBatch.size(); j++) {
                            try {
                                Map<String, Object> flatMap = flattenedBatch.get(j);
                                if (flatMap != null) {
                                    Row row = sheet.createRow(rowIdx);
                                    fillRowOptimized(row, flatMap, fieldNames);
                                    rowIdx++;
                                }
                            } catch (IllegalArgumentException e) {
                                log.warn("Invalid data in row {} of batch {}: {} - Skipping row",
                                        j + 1, currentBatchNumber, e.getMessage());
                                rowIdx++; // 행 번호는 증가시켜서 순서 유지
                            } catch (Exception e) {
                                log.warn("Failed to create row {} in batch {}: {} - Skipping row",
                                        j + 1, currentBatchNumber, e.getMessage());
                                rowIdx++; // 행 번호는 증가시켜서 순서 유지
                            }
                        }

                        processedDataSize += batch.size();

                        // 배치 완료 진행률 업데이트
                        int completedProgress = 40 + (int) ((processedDataSize * 50.0) / totalDataSize);
                        progressTracker.updateProgress(taskId, completedProgress, 100,
                                String.format("배치 %d 완료... (%,d/%,d)", currentBatchNumber, processedDataSize, totalDataSize));

                    } catch (OutOfMemoryError e) {
                        log.error("Out of memory during batch {} processing for taskId: {}", currentBatchNumber, taskId, e);
                        progressTracker.updateProgress(taskId, -1, 100, "메모리 부족으로 처리 중단됨");
                        throw new IOException("메모리 부족으로 Excel 생성을 완료할 수 없습니다. 데이터 크기를 줄여주세요.", e);
                    } catch (Exception e) {
                        log.error("Batch {} processing failed for taskId: {}", currentBatchNumber, taskId, e);

                        // 기타 오류는 경고 로그 후 계속 진행
                        progressTracker.updateProgress(taskId,
                                40 + (int) ((processedDataSize * 50.0) / totalDataSize), 100,
                                String.format("배치 %d 오류 발생, 계속 진행... (%,d/%,d)",
                                        currentBatchNumber, processedDataSize, totalDataSize));
                        processedDataSize += batch.size(); // 오류 발생한 배치도 처리된 것으로 간주
                    }

                    // 주기적 메모리 체크 및 가비지 컬렉션
                    try {
                        if (currentBatchNumber % 5 == 0) { // 5배치마다 체크
                            log.info("Excel processing: {:,} / {:,} rows (Batch: {})",
                                    processedDataSize, totalDataSize, currentBatchNumber);

                            // 메모리 상태 체크
                            Runtime runtime = Runtime.getRuntime();
                            long usedMemory = runtime.totalMemory() - runtime.freeMemory();
                            long maxMemory = runtime.maxMemory();
                            double memoryUsage = (double) usedMemory / maxMemory * 100;

                            if (memoryUsage > 85) {
                                log.warn("High memory usage detected: {:.1f}% for taskId: {}", memoryUsage, taskId);
                                progressTracker.updateProgress(taskId,
                                        40 + (int) ((processedDataSize * 50.0) / totalDataSize), 100,
                                        String.format("메모리 사용량 높음 ({:.1f}%%). 최적화 중...", memoryUsage));
                                System.gc();

                                // 메모리 체크 후 재확인
                                usedMemory = runtime.totalMemory() - runtime.freeMemory();
                                memoryUsage = (double) usedMemory / maxMemory * 100;

                                if (memoryUsage > 90) {
                                    log.error("Critical memory usage: {:.1f}% for taskId: {}", memoryUsage, taskId);
                                    throw new OutOfMemoryError("메모리 사용량이 위험 수준에 도달했습니다.");
                                }
                            }
                        }
                    } catch (OutOfMemoryError e) {
                        throw e;
                    } catch (Exception memoryCheckException) {
                        log.error("Failed to check memory status for taskId: {}", taskId, memoryCheckException);
                    }
                }

            } catch (OutOfMemoryError e) {
                log.error("Out of memory during data processing for taskId: {}", taskId, e);
                progressTracker.updateProgress(taskId, -1, 100, "메모리 부족으로 처리 중단됨");
                throw new IOException("메모리 부족으로 Excel 생성을 완료할 수 없습니다. 데이터 크기를 줄여주세요.", e);

            } catch (Exception e) {
                log.error("Data processing failed for taskId: {}", taskId, e);
                progressTracker.updateProgress(taskId, -1, 100, "데이터 처리 실패: " + e.getMessage());
                throw new IOException("데이터 처리 중 오류가 발생했습니다: " + e.getMessage(), e);
            }

            // 칼럼 너비 설정 (autoSize 대신 예상 너비 사용)
            progressTracker.updateProgress(taskId, 95, 100, "칼럼 너비 조정 중...");
            setOptimizedColumnWidths(sheet, headers, fieldNames);

            // ByteArrayOutputStream을 사용하여 메모리에서 파일 생성 후 전송
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
                workbook.write(baos);
                byte[] excelData = baos.toByteArray();

                progressTracker.updateProgress(taskId, 98, 100,
                        String.format("파일 전송 중... (크기: %.1fMB)", excelData.length / 1024.0 / 1024.0));

                // 응답으로 전송
                response.getOutputStream().write(excelData);
                response.getOutputStream().flush();
            }

            progressTracker.updateProgress(taskId, 100, 100,
                    String.format("다운로드 완료! (총 %,d건, %d개 컬럼)", totalDataSize, fieldNames.length));

            log.info("Excel generation completed successfully for taskId: {} ({} rows, {} columns)",
                    taskId, totalDataSize, fieldNames.length);

        }catch (IOException e) {
            // IOException은 그대로 다시 던지기
            throw e;
        } catch (Exception e) {
            // 기타 모든 예외를 IOException으로 래핑
            log.error("Unexpected error during Excel generation for taskId: {}", taskId, e);
            progressTracker.updateProgress(taskId, -1, 100, "예상치 못한 오류 발생: " + e.getMessage());
            throw new IOException("Excel 생성 중 예상치 못한 오류가 발생했습니다: " + e.getMessage(), e);

        }finally {
            // 리소스 정리
            try {
                if (workbook != null) {
                    workbook.close(); // XSSFWorkbook의 리소스 정리
                    log.debug("Workbook resources cleaned up for taskId: {}", taskId);
                }
            } catch (Exception cleanupException) {
                log.error("Failed to cleanup workbook resources for taskId: {}", taskId, cleanupException);
            }

            // 진행률 추적기에서 실패한 작업 정리 (5초 후)
            if (progressTracker.getProgress(taskId) != null &&
                    progressTracker.getProgress(taskId).getCurrentStep() == -1) {

                CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS).execute(() -> {
                    progressTracker.removeProgress(taskId);
                    log.info("Cleaned up failed task progress for taskId: {}", taskId);
                });
            }
        }
    }

    /**
     * 병렬 처리로 다중 엑셀 파일 ZIP 생성
     */
    private void generateMultipleExcelFilesAsZipParallel(HttpServletResponse response, List<?> data,
                                                         String fileName, String sheetName,
                                                         int maxRowsPerFile, String taskId) throws IOException {

        setupZipResponseHeaders(response, fileName + ".zip");
        progressTracker.updateProgress(taskId, 35, 100, "헤더 정보 생성중...");
        Map<String, String> headers = generateOptimizedHeaders(data);

        try (ZipOutputStream zipOut = new ZipOutputStream(response.getOutputStream())) {
            int totalFiles = (int) Math.ceil((double) data.size() / maxRowsPerFile);
            progressTracker.updateProgress(taskId, 40, 100,
                    String.format("ZIP 파일 생성 준비 중... (총 %d개 파일)", totalFiles));

            AtomicInteger completedFiles = new AtomicInteger(0);
            AtomicInteger zipWrittenFiles = new AtomicInteger(0);

            // 청크를 병렬로 처리
            List<CompletableFuture<ExcelFileData>> futures = new ArrayList<>();

            for (int fileIndex = 0; fileIndex < totalFiles; fileIndex++) {
                int startIndex = fileIndex * maxRowsPerFile;
                int endIndex = Math.min(startIndex + maxRowsPerFile, data.size());
                List<?> chunk = data.subList(startIndex, endIndex);

                final int currentFileIndex = fileIndex;
                CompletableFuture<ExcelFileData> future = CompletableFuture.supplyAsync(() -> {
                    try {
                        String excelFileName = String.format("%s_part%d.xlsx",
                                fileName, currentFileIndex + 1);

                        byte[] excelData = createSingleExcelFileBytesOptimized(chunk, headers, sheetName);

                        int completed = completedFiles.incrementAndGet();
                        progressTracker.updateProgress(taskId,
                                40 + (int) ((completed * 50.0) / totalFiles), 100,
                                String.format("Excel 파일 완료... (%d/%d)", completed, totalFiles));

                        return new ExcelFileData(excelFileName, excelData, currentFileIndex);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }, executorService);

                futures.add(future);
                if (futures.size() % 3 == 0) {
                    try {
                        Thread.sleep(50); // 50ms 대기
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }

            // 완료된 순서대로 ZIP에 추가 (순서 보장)
            for (int i = 0; i < futures.size(); i++) {
                try {
                    ExcelFileData fileData = futures.get(i).get();

                    ZipEntry zipEntry = new ZipEntry(fileData.fileName);
                    zipOut.putNextEntry(zipEntry);
                    zipOut.write(fileData.data);
                    zipOut.closeEntry();

                    int zipCompleted = zipWrittenFiles.incrementAndGet();
                    progressTracker.updateProgress(taskId,
                            90 + (int) ((zipCompleted * 5.0) / totalFiles), 100,
                            String.format("ZIP 압축 완료... (%d/%d)", zipCompleted, totalFiles));

                } catch (Exception e) {
                    log.error("Error processing file {}: {}", i, e.getMessage(), e);
                    throw new IOException("Excel file creation failed", e);
                }
            }

            zipOut.finish();
            progressTracker.updateProgress(taskId, 100, 100, "ZIP 파일 생성 완료!");
        }
    }

    /**
     * 현재 클래스와 상위 클래스의 모든 필드 수집
     * @param clazz 대상 클래스
     * @return 모든 필드 목록
     */
    private List<Field> getAllFields(Class<?> clazz) {
        List<Field> fields = new ArrayList<>();

        while (clazz != null && clazz != Object.class) {
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            clazz = clazz.getSuperclass();
        }

        return fields;
    }

    private String translateFieldName(String fieldName) {
        Map<String, String> translations = getFieldTranslations();

        return translations.getOrDefault(fieldName.toLowerCase(), fieldName);
    }

    /**
     * 필드명 한글 변환 맵 (필요에 따라 확장 가능)
     * @return 필드명 변환 맵
     */
    private Map<String, String> getFieldTranslations() {
        Map<String, String> translations = new HashMap<>();

        // 공통 필드명 변환
        translations.put("id", "ID");
        translations.put("name", "이름");
        translations.put("email", "이메일");
        translations.put("phone", "전화번호");
        translations.put("phonenumber", "전화번호");
        translations.put("address", "주소");
        translations.put("createdate", "생성일");
        translations.put("createddate", "생성일");
        translations.put("createdat", "생성일");
        translations.put("updatedate", "수정일");
        translations.put("updateddate", "수정일");
        translations.put("updatedat", "수정일");
        translations.put("registrationdate", "등록일");
        translations.put("regdate", "등록일");
        translations.put("birthdate", "생년월일");
        translations.put("age", "나이");
        translations.put("gender", "성별");
        translations.put("status", "상태");
        translations.put("type", "유형");
        translations.put("category", "카테고리");
        translations.put("description", "설명");
        translations.put("title", "제목");
        translations.put("content", "내용");
        translations.put("price", "가격");
        translations.put("amount", "금액");
        translations.put("quantity", "수량");
        translations.put("count", "개수");

        return translations;
    }

    /**
     * HttpServletResponse 헤더 설정
     */
    private void setupResponseHeaders(HttpServletResponse response, String fileName) throws IOException {
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
        response.setHeader("Cache-Control", "max-age=0");
    }

    /**
     * CSV 응답 헤더 설정
     */
    private void setupCsvResponseHeaders(HttpServletResponse response, String fileName) throws IOException {
        try {
            String cleanFileName = fileName + ".csv";
            String encodedFileName = URLEncoder.encode(fileName + ".csv", StandardCharsets.UTF_8)
                    .replaceAll("\\+", "%20");

            response.setContentType("text/csv; charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-Disposition",
                    String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s",
                            cleanFileName, encodedFileName));
            response.setHeader("Cache-Control", "max-age=0");
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Expires", "0");
        } catch (Exception e) {
            log.error("Failed to setup CSV response headers for fileName: {}", fileName, e);
            throw new IOException("응답 헤더 설정 실패: " + e.getMessage(), e);
        }
    }

    /**
     * ZIP 파일 응답 헤더 설정
     */
    private void setupZipResponseHeaders(HttpServletResponse response, String fileName) throws IOException {
        String cleanFileName = fileName.endsWith(".zip") ? fileName : fileName + ".zip";
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");

        response.setContentType("application/zip");
        response.setHeader("Content-Disposition",
                String.format("attachment; filename=\"%s\"; filename*=UTF-8''%s",
                        cleanFileName, encodedFileName));
        response.setHeader("Cache-Control", "max-age=0");
    }

    /**
     * 헤더 스타일 생성
     */
    private CellStyle createHeaderStyle(Workbook workbook) {
        CellStyle headerStyle = workbook.createCellStyle();
        Font headerFont = workbook.createFont();
        headerFont.setBold(true);
        headerStyle.setFont(headerFont);
        headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        headerStyle.setBorderBottom(BorderStyle.THIN);
        headerStyle.setBorderTop(BorderStyle.THIN);
        headerStyle.setBorderLeft(BorderStyle.THIN);
        headerStyle.setBorderRight(BorderStyle.THIN);
        headerStyle.setAlignment(HorizontalAlignment.CENTER);
        return headerStyle;
    }

    /**
     * 헤더 행 생성
     */
    private void createHeaderRow(Sheet sheet, Map<String, String> headers, CellStyle headerStyle) {
        Row headerRow = sheet.createRow(0);
        int colIdx = 0;
        for (String header : headers.values()) {
            Cell cell = headerRow.createCell(colIdx++);
            cell.setCellValue(header);
            cell.setCellStyle(headerStyle);
        }
    }

    /**
     * 데이터 행 생성 (객체 리스트용) - Map 필드 자동 펼치기 지원
     */
    private void createDataRows(Sheet sheet, List<?> data, String[] fieldNames) {
        int rowIdx = 1;
        for (Object item : data) {
            Row row = sheet.createRow(rowIdx++);

            // 객체를 펼친 Map으로 변환
            Map<String, Object> flatMap = flattenObjectToMap(item);

            // 헤더 순서대로 데이터 채우기
            int colIdx = 0;
            for (String fieldName : fieldNames) {
                Cell cell = row.createCell(colIdx++);
                Object value = flatMap.get(fieldName);
                setCellValue(cell, value);
            }
        }
    }

    /**
     * 칼럼 너비 자동 조정
     */
    private void autoSizeColumns(Sheet sheet, int columnCount) {
        for (int i = 0; i < columnCount; i++) {
            sheet.autoSizeColumn(i);
        }
    }

    /**
     * 객체 타입에 따라 셀 값 설정
     */
    private void setCellValue(Cell cell, Object value) {
        if (value == null) {
            cell.setCellValue("");
        } else if (value instanceof String) {
            cell.setCellValue((String) value);
        } else if (value instanceof Number) {
            if (value instanceof Integer || value instanceof Long) {
                cell.setCellValue(((Number) value).longValue());
            } else {
                cell.setCellValue(((Number) value).doubleValue());
            }
        } else if (value instanceof Boolean) {
            cell.setCellValue((Boolean) value);
        } else if (value instanceof java.util.Date) {
            cell.setCellValue((java.util.Date) value);

            // 날짜 형식 스타일 적용
            CellStyle dateStyle = cell.getSheet().getWorkbook().createCellStyle();
            CreationHelper createHelper = cell.getSheet().getWorkbook().getCreationHelper();
            dateStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-mm-dd"));
            cell.setCellStyle(dateStyle);
        } else {
            cell.setCellValue(value.toString());
        }
    }

    /**
     * Map을 재귀적으로 펼치는 메서드
     */
    private void flattenMap(Map<String, Object> map, String prefix, Map<String, Object> result) {
        if (map == null || map.isEmpty()) return;

        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            String newKey = prefix + "." + key;

            if (value instanceof Map && !((Map<?, ?>) value).isEmpty()) {
                // 중첩 Map인 경우 재귀 호출
                flattenMap((Map<String, Object>) value, newKey, result);
            } else if (value instanceof List && !((List<?>) value).isEmpty()) {
                // List인 경우 처리
                List<?> list = (List<?>) value;
                Object firstItem = list.get(0);
                if (firstItem instanceof Map) {
                    // List의 첫 번째 Map 요소를 펼치기
                    flattenMap((Map<String, Object>) firstItem, newKey + "[0]", result);
                } else {
                    // 일반 List는 문자열로 변환
                    result.put(newKey, value.toString());
                }
            } else if (value != null) {
                // 일반 값
                result.put(newKey, value);
            }
        }
    }

    /**
     * 배치를 미리 변환하여 성능 향상
     */
    private List<Map<String, Object>> preProcessBatch(List<?> batch) {
        return batch.parallelStream()
                .filter(Objects::nonNull)
                .map(this::flattenObjectToMapOptimized)
                .collect(Collectors.toList());
    }

    /**
     * 최적화된 헤더 생성 (샘플링 사용)
     */
    private Map<String, String> generateOptimizedHeaders(List<?> data) {
        if (data == null || data.isEmpty()) {
            log.warn("Data is null or empty for header generation");
            return new LinkedHashMap<>();
        }

        try {
            // 샘플링으로 헤더 생성 (최대 100개만 검사)
            int sampleSize = Math.min(100, data.size());
            List<?> sampleData = data.subList(0, sampleSize);;

            // ExcelBusinessLog 타입인 경우 도메인별 샘플링
            if (!sampleData.isEmpty() && sampleData.get(0) instanceof ExcelBusinessLog) {
                List<ExcelBusinessLog> businessLogs = (List<ExcelBusinessLog>) sampleData;
                sampleData = businessLogs.stream()
                        .filter(Objects::nonNull)
                        .filter(log -> log.getDomain() != null)
                        .collect(Collectors.groupingBy(
                                ExcelBusinessLog::getDomain,
                                LinkedHashMap::new,
                                Collectors.toList()
                        ))
                        .values()
                        .stream()
                        .map(list -> list.get(0))
                        .collect(Collectors.toList());
            }

            return generateHeadersFromSampleObjects(sampleData);
        }catch (Exception e) {
            log.error("Unexpected error during header generation", e);
            return generateFallbackHeaders(data);
        }
    }

    /**
     * 최적화된 객체 플래튼화 (캐시 활용)
     */
    private Map<String, Object> flattenObjectToMapOptimized(Object item) {
        Map<String, Object> flatMap = new LinkedHashMap<>();

        if (item == null) return flatMap;

        Class<?> clazz = item.getClass();
        List<Field> allFields = getCachedFields(clazz);

        for (Field field : allFields) {
            if (shouldSkipField(field)) continue;

            try {
                field.setAccessible(true);
                Object fieldValue = field.get(item);
                String fieldName = field.getName();

                if ("message".equals(fieldName)) continue;

                if (fieldValue instanceof Map) {
                    Map<String, Object> nestedMap = (Map<String, Object>) fieldValue;
                    flattenMapOptimized(nestedMap, fieldName, flatMap);
                } else if (fieldValue != null) {
                    flatMap.put(fieldName, fieldValue);
                }
            } catch (Exception e) {
                // 접근할 수 없는 필드는 무시
            }
        }

        return flatMap;
    }

    /**
     * 필드 캐시 활용
     */
    private List<Field> getCachedFields(Class<?> clazz) {
        return fieldCache.computeIfAbsent(clazz, this::getAllFields);
    }

    /**
     * 필드 스킵 여부 판단
     */
    private boolean shouldSkipField(Field field) {
        int modifiers = field.getModifiers();
        return java.lang.reflect.Modifier.isStatic(modifiers) ||
                java.lang.reflect.Modifier.isFinal(modifiers);
    }

    /**
     * 최적화된 Map 플래튼화
     */
    private void flattenMapOptimized(Map<String, Object> map, String prefix, Map<String, Object> result) {
        if (map == null || map.isEmpty()) return;

        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            String newKey = prefix + "." + key;

            if (value instanceof Map && !((Map<?, ?>) value).isEmpty()) {
                flattenMapOptimized((Map<String, Object>) value, newKey, result);
            } else if (value instanceof List && !((List<?>) value).isEmpty()) {
                List<?> list = (List<?>) value;
                Object firstItem = list.get(0);
                if (firstItem instanceof Map) {
                    flattenMapOptimized((Map<String, Object>) firstItem, newKey + "[0]", result);
                } else {
                    result.put(newKey, value.toString());
                }
            } else if (value != null) {
                result.put(newKey, value);
            }
        }
    }

    /**
     * 최적화된 행 채우기
     */
    private void fillRowOptimized(Row row, Map<String, Object> flatMap, String[] fieldNames) {
        for (int i = 0; i < fieldNames.length; i++) {
            Cell cell = row.createCell(i);
            Object value = flatMap.get(fieldNames[i]);
            setCellValueOptimized(cell, value);
        }
    }

    /**
     * 최적화된 셀 값 설정
     */
    private void setCellValueOptimized(Cell cell, Object value) {
        if (value == null) {
            cell.setCellValue("");
            return;
        }

        // 타입별로 최적화된 처리
        if (value instanceof String) {
            cell.setCellValue((String) value);
        } else if (value instanceof Integer) {
            cell.setCellValue(((Integer) value).longValue());
        } else if (value instanceof Long) {
            cell.setCellValue((Long) value);
        } else if (value instanceof Double) {
            cell.setCellValue((Double) value);
        } else if (value instanceof Float) {
            cell.setCellValue(((Float) value).doubleValue());
        } else if (value instanceof Boolean) {
            cell.setCellValue((Boolean) value);
        } else if (value instanceof java.util.Date) {
            cell.setCellValue((java.util.Date) value);
            // 날짜 스타일은 워크북 레벨에서 한 번만 생성하여 재사용
            applyCachedDateStyle(cell);
        } else {
            cell.setCellValue(value.toString());
        }
    }

    /**
     * 최적화된 칼럼 너비 설정 (autoSize 대신 예상 너비 사용)
     */
    private void setOptimizedColumnWidths(Sheet sheet, Map<String, String> headers, String[] fieldNames) {
        for (int i = 0; i < fieldNames.length; i++) {
            String fieldName = fieldNames[i];
            String headerText = headers.get(fieldName);

            // 예상 너비 계산 (autoSize보다 빠름)
            int estimatedWidth = estimateColumnWidth(fieldName, headerText);
            sheet.setColumnWidth(i, estimatedWidth);
        }
    }

    /**
     * 칼럼 너비 추정
     */
    private int estimateColumnWidth(String fieldName, String headerText) {
        int baseWidth = Math.max(headerText.length() * 256, 2560);

        String lowerFieldName = fieldName.toLowerCase();
        if (lowerFieldName.contains("guid") || lowerFieldName.contains("tran") || lowerFieldName.contains("nickname")) {
            return Math.min(8000, 65280);
        } else if (lowerFieldName.contains("date") || lowerFieldName.contains("time")) {
            return Math.min(6000, 65280);
        } else if (lowerFieldName.contains("action") || lowerFieldName.contains("domain") || lowerFieldName.contains("account")) {
            return Math.min(3000, 65280);
        }

        return Math.min(baseWidth, 65280);
    }

    /**
     * 캐시된 날짜 스타일 적용
     */
    private void applyCachedDateStyle(Cell cell) {
        // 날짜 스타일 캐시 구현 (워크북당 한 번만 생성)
        Workbook workbook = cell.getSheet().getWorkbook();
        // 실제 구현에서는 워크북별 스타일 캐시 사용
    }

    // Excel 파일 데이터를 담는 내부 클래스
    private static class ExcelFileData {
        final String fileName;
        final byte[] data;
        final int fileIndex;

        ExcelFileData(String fileName, byte[] data, int fileIndex) {
            this.fileName = fileName;
            this.data = data;
            this.fileIndex = fileIndex;
        }
    }

    /**
     * 최적화된 단일 엑셀 파일 바이트 생성
     */
    private byte[] createSingleExcelFileBytesOptimized(List<?> data, Map<String, String> headers,
                                                       String sheetName) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
             XSSFWorkbook workbook = new XSSFWorkbook()) {

            Sheet sheet = workbook.createSheet(sheetName);
            CellStyle headerStyle = createHeaderStyle(workbook);

            createHeaderRow(sheet, headers, headerStyle);
            String[] fieldNames = headers.keySet().toArray(new String[0]);

            // 배치 처리로 최적화
            List<Map<String, Object>> flattenedData = preProcessBatch(data);

            int rowIdx = 1;
            for (Map<String, Object> flatMap : flattenedData) {
                Row row = sheet.createRow(rowIdx++);
                fillRowOptimized(row, flatMap, fieldNames);
            }

            setOptimizedColumnWidths(sheet, headers, fieldNames);
            workbook.write(baos);
            return baos.toByteArray();
        }
    }

    /**
     * Object 리스트의 모든 객체를 검사하여 헤더 생성 (Map 필드는 자동으로 펼치기)
     * @param data 데이터 리스트
     * @return 모든 필드를 포함한 헤더 Map
     */
    private Map<String, String> generateHeadersFromSampleObjects(List<?> data) {
        LinkedHashMap<String, String> headers = new LinkedHashMap<>();
        Set<String> allFieldKeys = new LinkedHashSet<>();

        if (data == null || data.isEmpty()) {
            return headers;
        }

        for (Object item : data) {
            if (item != null) {
                Map<String, Object> flatMap = flattenObjectToMapOptimized(item);
                allFieldKeys.addAll(flatMap.keySet());
            }
        }

        for (String key : allFieldKeys) {
            if (EXCLUDED_HEADERS.contains(key)) continue;

            String headerName = convertFlatKeyToHeader(key);
            headers.put(key, headerName);
        }

        return headers;
    }


    /**
     * 펼쳐진 키를 읽기 쉬운 헤더명으로 변환
     */
    private String convertFlatKeyToHeader(String key) {
        return translationCache.computeIfAbsent(key, this::computeHeaderName);
    }

    private String computeHeaderName(String key) {
        String[] parts = key.split("\\.");
        StringBuilder result = new StringBuilder();

        for (int i = 0; i < parts.length; i++) {
            if (i > 0) result.append(" ");

            String part = parts[i].replaceAll("\\[\\d+\\]", "");
            String translatedPart = translateFieldName(part);
            result.append(translatedPart);
        }

        return result.toString();
    }

    /**
     * 객체를 펼친 Map으로 변환
     */
    private Map<String, Object> flattenObjectToMap(Object item) {
        Map<String, Object> flatMap = new LinkedHashMap<>();

        if (item == null) return flatMap;

        Class<?> clazz = item.getClass();
        List<Field> allFields = getAllFields(clazz);

        for (Field field : allFields) {
            if (java.lang.reflect.Modifier.isStatic(field.getModifiers()) ||
                    java.lang.reflect.Modifier.isFinal(field.getModifiers())) {
                continue;
            }

            try {
                field.setAccessible(true);
                Object fieldValue = field.get(item);
                String fieldName = field.getName();

                // message 필드는 제외 (이미 header, body로 분리됨)
                if ("message".equals(fieldName)) {
                    continue;
                }

                if (fieldValue instanceof Map) {
                    // Map 필드를 펼치기
                    Map<String, Object> nestedMap = (Map<String, Object>) fieldValue;
                    flattenMap(nestedMap, fieldName, flatMap);
                } else if (fieldValue != null) {
                    // 일반 필드는 그대로 추가
                    flatMap.put(fieldName, fieldValue);
                }
            } catch (Exception e) {
                // 접근할 수 없는 필드는 무시
            }
        }

        return flatMap;
    }

    /**
     * 폴백 헤더 생성 (기본 헤더)
     */
    private Map<String, String> generateFallbackHeaders(List<?> data) {
        Map<String, String> fallbackHeaders = new LinkedHashMap<>();
        try {

            // 기본 헤더들 추가
            fallbackHeaders.put("logTime", "로그 시간");
            fallbackHeaders.put("accountId", "계정 ID");
            fallbackHeaders.put("userGuid", "사용자 GUID");
            fallbackHeaders.put("userNickname", "사용자 닉네임");
            fallbackHeaders.put("tranId", "거래 ID");
            fallbackHeaders.put("action", "액션");
            fallbackHeaders.put("domain", "도메인");

            log.info("Generated {} fallback headers", fallbackHeaders.size());

        } catch (Exception e) {
            log.error("Failed to generate fallback headers", e);
            // 최후의 수단으로 매우 기본적인 헤더만 제공
            fallbackHeaders.put("data", "데이터");
        }

        return fallbackHeaders;
    }

    /**
     * 인덱스 파일 생성
     */
    private void createIndexFile(ZipOutputStream zipOut, String baseFileName, int totalFiles,
                                 int totalRecords, int maxRowsPerFile) throws IOException {

        StringBuilder indexContent = new StringBuilder();
        indexContent.append("=== CSV 파일 다운로드 정보 ===\n\n");
        indexContent.append("파일명: ").append(baseFileName).append("\n");
        indexContent.append("총 데이터 건수: ").append(String.format("%,d", totalRecords)).append("건\n");
        indexContent.append("분할된 파일 수: ").append(totalFiles).append("개\n");
        indexContent.append("파일당 최대 행 수: ").append(String.format("%,d", maxRowsPerFile)).append("행\n");
        indexContent.append("생성 일시: ").append(java.time.LocalDateTime.now().format(
                java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).append("\n\n");

        indexContent.append("=== 파일 목록 ===\n");
        for (int i = 1; i <= totalFiles; i++) {
            int startRecord = (i - 1) * maxRowsPerFile + 1;
            int endRecord = Math.min(i * maxRowsPerFile, totalRecords);
            indexContent.append(String.format("📁 %s_part_%d_of_%d.xlsx\n", baseFileName, i, totalFiles));
            indexContent.append(String.format("   └─ %,d ~ %,d번째 데이터 (%,d건)\n\n",
                    startRecord, endRecord, endRecord - startRecord + 1));
        }

        indexContent.append("=== 사용 안내 ===\n");
        indexContent.append("• 각 CSV 파일을 개별적으로 열어서 사용하세요.\n");
        indexContent.append("• 파일명의 숫자는 데이터 순서를 나타냅니다.\n");
        indexContent.append("• 모든 파일의 헤더 구조는 동일합니다.\n");
        indexContent.append("• Excel에서 열 때는 '데이터 > 텍스트 나누기'로 열어주세요.\n");

        ZipEntry indexEntry = new ZipEntry("📋 파일정보_및_사용안내.txt");
        zipOut.putNextEntry(indexEntry);
        zipOut.write(indexContent.toString().getBytes(StandardCharsets.UTF_8));
        zipOut.closeEntry();
    }

    private static class CsvFileData {
        final String fileName;
        final byte[] data;
        final int fileIndex;

        CsvFileData(String fileName, byte[] data, int fileIndex) {
            this.fileName = fileName;
            this.data = data;
            this.fileIndex = fileIndex;
        }
    }
}
