package go.kr.project.carInspectionPenalty.registrationOm.service.impl;

import egovframework.constant.TaskPrcsSttsConstants;
import egovframework.exception.MessageException;
import egovframework.util.SessionUtil;
import go.kr.project.carInspectionPenalty.registrationOm.config.CarFfnlgPrnParseConfig;
import go.kr.project.carInspectionPenalty.registrationOm.mapper.CarFfnlgTrgtIncmpMapper;
import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpExcelVO;
import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpModifiedDataVO;
import go.kr.project.carInspectionPenalty.registrationOm.model.CarFfnlgTrgtIncmpVO;
import go.kr.project.carInspectionPenalty.registrationOm.service.CarFfnlgTrgtIncmpService;
import go.kr.project.carInspectionPenalty.registrationOm.service.ComparisonOmService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.multipart.MultipartFile;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * 자동차 과태료 대상 미필 Service 구현체
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class CarFfnlgTrgtIncmpServiceImpl extends EgovAbstractServiceImpl implements CarFfnlgTrgtIncmpService {

    private final CarFfnlgTrgtIncmpMapper mapper;
    private final CarFfnlgPrnParseConfig parseConfig;
    private final ComparisonOmService comparisonOmService;
    private final TransactionTemplate transactionTemplate;

    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
    private static final DateTimeFormatter DATE_FORMATTER_DASH = DateTimeFormatter.ofPattern("yyyy-MM-dd");

    @Override
    public int selectListTotalCount(CarFfnlgTrgtIncmpVO vo) {
        return mapper.selectListTotalCount(vo);
    }

    @Override
    public List<CarFfnlgTrgtIncmpVO> selectList(CarFfnlgTrgtIncmpVO vo) {
        return mapper.selectList(vo);
    }

    @Override
    public CarFfnlgTrgtIncmpVO selectOne(CarFfnlgTrgtIncmpVO vo) {
        return mapper.selectOne(vo);
    }

    @Override
    @Transactional
    public int insert(CarFfnlgTrgtIncmpVO vo) {
        return mapper.insert(vo);
    }

    @Override
    @Transactional
    public int update(CarFfnlgTrgtIncmpVO vo) {
        return mapper.update(vo);
    }

    @Override
    @Transactional
    public int delete(CarFfnlgTrgtIncmpVO vo) {
        return mapper.delete(vo);
    }

    /**
     * PRN 파일 업로드 및 파싱하여 DB 저장
     *
     * 파일 형식: 고정폭 PRN 파일 (헤더 3줄 + 데이터 2줄씩)
     * - 헤더 1줄: 프로그램ID
     * - 헤더 2줄: 처리일자
     * - 헤더 3줄: 출력일시
     * - 데이터: 4번째 라인부터 시작, 2줄 1세트
     *   1) 첫 번째 줄: 번호/차량번호/소유자명/차명/사용본거지주소/검사유효기간
     *   2) 두 번째 줄: 주민등록번호/사용본거지주소(나머지)
     *
     * 주의: 파일 단위로 업로드하므로 한 건이라도 실패하면 전체 롤백 처리됨
     */
    @Override
    @Transactional
    public Map<String, Object> uploadAndParsePrnFile(MultipartFile file, String rgtr) {
        Map<String, Object> result = new HashMap<>();
        List<String> errorMessages = new ArrayList<>();
        int successCount = 0;
        int dataLineNumber = 0;

        try {
            // 파일 검증
            if (file == null || file.isEmpty()) {
                throw new IllegalArgumentException("파일이 선택되지 않았습니다.");
            }

            String originalFilename = file.getOriginalFilename();
            if (originalFilename == null || (!originalFilename.toLowerCase().endsWith(".prn") && !originalFilename.toLowerCase().endsWith(".txt"))) {
                throw new IllegalArgumentException("PRN, TXT 파일만 업로드 가능합니다. 선택된 파일: " + originalFilename);
            }

            if (file.getSize() > 50 * 1024 * 1024) {
                throw new IllegalArgumentException("파일 크기는 50MB를 초과할 수 없습니다. 파일 크기: " + (file.getSize() / 1024 / 1024) + "MB");
            }

            log.info("PRN 파일 업로드 시작 - 파일명: {}, 크기: {} bytes", originalFilename, file.getSize());

            String encoding = parseConfig.getEncoding();
            log.info("파일 인코딩: {}, 한글 바이트 크기: {}", encoding, parseConfig.getHangulByteSize());

            List<String> allLines = new ArrayList<>();
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(file.getInputStream(), encoding))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    allLines.add(line);
                }
            }

            // 파일 최소 라인 검증 (헤더 7라인 + 데이터 최소 2라인)
            if (allLines.size() < 9) {
                throw new IllegalArgumentException("파일 형식이 올바르지 않습니다. 최소 9라인 이상이어야 합니다. 현재 라인 수: " + allLines.size());
            }

            log.info("파일 읽기 완료 - 총 라인 수: {}", allLines.size());

            // 헤더 파싱 (3, 4번째 줄에서 추출)
            // Line 3: "  프로그램  ID : VGD01B"
            // Line 4: "  처 리  일 자 : 2025년07월11일~2025년07월11일                    출 력  일 시 : 2025년12월04일17시01분"
            String line3 = allLines.get(2).trim();
            String line4 = allLines.get(3).trim();

            String prgrmId = "";
            String prcsYmd = "";
            String otptDt = "";

            // 프로그램 ID 추출 (: 기준으로 split)
            if (line3.contains(":")) {
                String[] parts = line3.split(":", 2);
                if (parts.length > 1) {
                    prgrmId = parts[1].trim();
                }
            }

            // 처리일자, 출력일시 추출
            if (line4.contains(":")) {
                String[] parts = line4.split(":", 2);
                if (parts.length > 1) {
                    String rest = parts[1].trim();
                    // "2025년07월11일~2025년07월11일                    출 력  일 시 : 2025년12월04일17시01분" 형태
                    if (rest.contains("출")) {
                        int idx = rest.indexOf("출");
                        prcsYmd = rest.substring(0, idx).trim();
                        String rest2 = rest.substring(idx);
                        if (rest2.contains(":")) {
                            String[] parts2 = rest2.split(":", 2);
                            if (parts2.length > 1) {
                                otptDt = parts2[1].trim();
                            }
                        }
                    } else {
                        prcsYmd = rest;
                    }
                }
            }

            log.info("헤더 정보 - 프로그램ID: {}, 처리일자: {}, 출력일시: {}", prgrmId, prcsYmd, otptDt);

            // 8번째 라인부터 데이터 처리 (인덱스 7부터 시작)
            for (int i = 7; i < allLines.size(); i++) {
                String firstLine = allLines.get(i);

                if (firstLine.trim().isEmpty()) {
                    continue;
                }

                if (firstLine.trim().startsWith("---")) {
                    continue;
                }

                if (i + 1 >= allLines.size()) {
                    String errorMsg = String.format("[라인 %d] 데이터가 불완전합니다. 2줄 1세트 형식이 필요합니다.", i + 1);
                    errorMessages.add(errorMsg);
                    throw new MessageException(buildErrorMessage(errorMessages));
                }

                String secondLine = allLines.get(i + 1);
                dataLineNumber++;

                // 고정폭 파싱
                CarFfnlgTrgtIncmpVO vo = parseFixedWidthData(firstLine, secondLine, dataLineNumber, errorMessages, prgrmId, prcsYmd, otptDt);

                if (vo == null) {
                    throw new MessageException(buildErrorMessage(errorMessages));
                }

                // 필수 필드 검증
                List<String> validationErrors = validateParsedData(dataLineNumber, vo);

                if (!validationErrors.isEmpty()) {
                    errorMessages.addAll(validationErrors);
                    throw new MessageException(buildErrorMessage(errorMessages));
                }

                // 차량번호+검사유효기간 중복 체크
                CarFfnlgTrgtIncmpVO checkVO = new CarFfnlgTrgtIncmpVO();
                checkVO.setVhclno(vo.getVhclno());
                checkVO.setInspVldPrd(vo.getInspVldPrd());
                int duplicateCount = mapper.checkDuplicateVhclno(checkVO);
                if (duplicateCount > 0) {
                    String errorMsg = String.format("[데이터 %d] 중복된 차량번호+검사유효기간입니다. 차량번호: %s, 검사유효기간: %s",
                            dataLineNumber, vo.getVhclno(), vo.getInspVldPrd());
                    errorMessages.add(errorMsg);
                    throw new MessageException(buildErrorMessage(errorMessages));
                }

                // 업무 처리 상태 및 등록자 설정
                vo.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT);
                vo.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER_DASH));
                vo.setRcptYmd(LocalDate.now().format(DATE_FORMATTER_DASH));
                vo.setRgtr(rgtr);

                // DB 저장
                int insertResult = mapper.insert(vo);

                if (insertResult > 0) {
                    successCount++;
                    log.debug("데이터 저장 성공 [데이터 {}] - 차량번호: {}", dataLineNumber, vo.getVhclno());
                } else {
                    String errorMsg = String.format("[데이터 %d] 데이터 저장 실패 - 차량번호: %s", dataLineNumber, vo.getVhclno());
                    errorMessages.add(errorMsg);
                    throw new MessageException(buildErrorMessage(errorMessages));
                }

                // 2줄 1세트이므로 다음 줄 건너뛰기
                i++;
            }

            log.info("PRN 파일 처리 완료 - 성공: {}건", successCount);

            result.put("success", true);
            result.put("successCount", successCount);
            result.put("failCount", 0);
            result.put("errorMessages", errorMessages);

        } catch (IllegalArgumentException e) {
            log.error("파일 검증 중 오류 발생: {}", e.getMessage());
            errorMessages.add(e.getMessage());
            result.put("success", false);
            result.put("successCount", 0);
            result.put("failCount", 0);
            result.put("errorMessages", errorMessages);
        } catch (MessageException e) {
            log.error("PRN 파일 업로드 중 오류 발생 - 전체 롤백 처리", e);
            throw e;
        } catch (Exception e) {
            log.error("PRN 파일 업로드 중 예상치 못한 오류 발생", e);
            errorMessages.add("파일 업로드 중 오류가 발생했습니다: " + e.getMessage());
            throw new MessageException(buildErrorMessage(errorMessages), e);
        }

        return result;
    }

    /**
     * 목록을 EUC-KR 텍스트로 생성하여 다운로드용 바이트 배열을 반환
     */
    @Override
    public byte[] generateEucKrDownloadBytes(CarFfnlgTrgtIncmpVO vo) {
        try {
            final String encoding = parseConfig.getEncoding() == null || parseConfig.getEncoding().trim().isEmpty()
                    ? "EUC-KR" : parseConfig.getEncoding().trim();

            List<CarFfnlgTrgtIncmpVO> list = mapper.selectList(vo);

            StringBuilder sb = new StringBuilder();

            // 1번째 줄: 제목 (가운데 정렬)
            String title = "자동차 검사 경과 안내서 대상차량 대장";
            sb.append(padLeftBytes(title, 106, encoding)).append("\r\n");

            // 2번째 줄: 구분선 (가운데 정렬)
            String titleLine = "---------------------------------------";
            sb.append(padLeftBytes(titleLine, 107, encoding)).append("\r\n");

            // 3번째 줄: 프로그램 ID
            String prgrmId = vo.getPrgrmId() != null ? vo.getPrgrmId() : "VGD01B";
            sb.append("  프로그램  ID : ").append(prgrmId);
            sb.append(padRightBytes("", 114 - ("  프로그램  ID : " + prgrmId).getBytes(encoding).length, encoding));
            sb.append("\r\n");

            // 4번째 줄: 처리일자 및 출력일시 (DB 값 그대로 사용)
            String prcsYmd = "";
            String otptDt = "";
            if (!list.isEmpty()) {
                prcsYmd = nvl(list.get(0).getPrcsYmd());
                otptDt = nvl(list.get(0).getOtptDt());
            }

            String line4 = String.format("  처 리  일 자 : %s", prcsYmd);
            String line4Right = String.format("출 력  일 시 : %s", otptDt);

            int line4LeftBytes = line4.getBytes(encoding).length;
            int line4RightBytes = line4Right.getBytes(encoding).length;
            int paddingBytes = 151 - line4LeftBytes - line4RightBytes;

            sb.append(line4);
            sb.append(padRightBytes("", paddingBytes, encoding));
            sb.append(line4Right).append("\r\n");

            // 5번째 줄: 긴 구분선
            sb.append(repeatChar('-', 159, encoding)).append("\r\n");

            // 6번째 줄: 컬럼 헤더
            sb.append("번호    자동차번호    소유자명        주민등록번호    차   명               사용 본거지 주소                                               검사유효기간    \r\n");

            // 7번째 줄: 긴 구분선
            sb.append(repeatChar('-', 159, encoding)).append("\r\n");

            // 8번째 줄부터: 데이터 (2줄 1세트)
            int rowNum = 1;
            for (CarFfnlgTrgtIncmpVO row : list) {
                // 첫 번째 줄
                // 번호: 4자리 오른쪽 정렬 + 공백 2자리 (예: "   1  ", "1011  ", " 123  ")
                String firstLine =
                        String.format("%4d  ", rowNum) +
                        padRightBytes(nvl(row.getVhclno()), 16, encoding) +
                        padRightBytes(nvl(row.getOwnrNm()), 32, encoding) +
                        padRightBytes(nvl(row.getCarNm()), 22, encoding) +
                        padRightBytes(nvl(row.getUseStrhldAddr()), 61, encoding) +
                        padRightBytes(nvl(row.getInspVldPrd()), 21, encoding);

                sb.append(firstLine).append("\r\n");

                // 두 번째 줄 (주민번호와 주소상세)
                String secondLine =
                        padRightBytes("", 38, encoding) +
                        padRightBytes(nvl(row.getRrno()), 38, encoding) +
                        padRightBytes(nvl(row.getUseStrhldAddrDtl()), 61, encoding);

                sb.append(secondLine).append("\r\n");

                rowNum++;
            }

            return sb.toString().getBytes(encoding);
        } catch (Exception e) {
            throw new MessageException("다운로드 파일 생성 중 오류: " + e.getMessage(), e);
        }
    }

    /**
     * 왼쪽 패딩 (문자열을 오른쪽에 배치)
     */
    private static String padLeftBytes(String s, int totalByteLen, String encoding) throws Exception {
        if (totalByteLen <= 0) return nvl(s);
        String v = nvl(s);
        byte[] b = v.getBytes(encoding);

        if (b.length >= totalByteLen) {
            return truncateToBytes(v, totalByteLen, encoding);
        }

        int paddingBytes = totalByteLen - b.length;
        StringBuilder sb = new StringBuilder();

        // 왼쪽 패딩
        for (int i = 0; i < paddingBytes; i++) {
            sb.append(' ');
        }
        sb.append(v);

        return sb.toString();
    }

    /**
     * 문자 반복
     */
    private static String repeatChar(char c, int totalByteLen, String encoding) throws Exception {
        StringBuilder sb = new StringBuilder();
        while (sb.toString().getBytes(encoding).length < totalByteLen) {
            sb.append(c);
        }
        // 바이트 길이 정확히 맞추기
        while (sb.toString().getBytes(encoding).length > totalByteLen) {
            sb.deleteCharAt(sb.length() - 1);
        }
        return sb.toString();
    }

    /**
     * 선택된 목록에 대해 API 호출 및 기본정보/등록원부 비교 (병렬처리)
     * 미필의 경우 부과일자 = 검사유효기간 종료일 + OM_DAY_CD의 D 코드값(146일)
     * 변경사항: 각 건을 병렬로 처리하며, 개별 트랜잭션으로 관리됩니다.
     * 실패한 건만 실패 처리되고, 성공한 건은 정상 처리됩니다.
     *
     * @param targetList 선택된 과태료 대상 목록
     * @return 비교 결과
     */
    @Override
    public Map<String, Object> compareWithApi(List<Map<String, String>> targetList) {
        log.info("========== 미필 API 호출 및 비교 시작 (병렬처리) ==========");
        log.info("선택된 데이터 건수: {}", targetList != null ? targetList.size() : 0);

        if (targetList == null || targetList.isEmpty()) {
            throw new IllegalArgumentException("선택된 데이터가 없습니다.");
        }

        // 가산일 조회 (OM_DAY_CD 코드의 D값) - 병렬처리 전에 미리 조회
        String plusDayStr = mapper.selectOmDayPlusDay();
        final int plusDay;
        if (plusDayStr != null && !plusDayStr.isEmpty()) {
            int parsed = 146;
            try {
                parsed = Integer.parseInt(plusDayStr);
            } catch (NumberFormatException e) {
                log.warn("가산일 파싱 실패, 기본값 146 사용: {}", plusDayStr);
            }
            plusDay = parsed;
        } else {
            plusDay = 146; // 기본값
        }
        log.info("부과일자 가산일: {}일", plusDay);

        // 세션에서 사용자 정보 조회 (병렬처리 전에 메인 스레드에서 조회)
        go.kr.project.login.model.LoginUserVO userInfo = null;
        try {
            userInfo = egovframework.util.SessionUtil.getLoginUser();
        } catch (Exception e) {
            log.warn("세션에서 사용자 정보 조회 실패", e);
        }
        final go.kr.project.login.model.LoginUserVO finalUserInfo = userInfo;

        // I/O 작업이므로 CPU 코어 수의 2배로 스레드 풀 생성
        int threadPoolSize = Runtime.getRuntime().availableProcessors() * 2;
        ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);

        log.info("병렬처리 스레드 풀 크기: {}", threadPoolSize);

        try {
            // 병렬로 각 건 처리
            List<CompletableFuture<Map<String, Object>>> futures = targetList.stream()
                    .map(target -> CompletableFuture.supplyAsync(() -> processOneTarget(target, plusDay, finalUserInfo), executor))
                    .collect(Collectors.toList());

            // 모든 작업 완료 대기
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

            // 결과 수집
            List<Map<String, Object>> compareResults = futures.stream()
                    .map(CompletableFuture::join)
                    .collect(Collectors.toList());

            // 통계 집계
            int successCount = 0;
            int failCount = 0;
            int productUseCount = 0;
            int transferCount = 0;
            int normalCount = 0;

            for (Map<String, Object> result : compareResults) {
                Boolean success = (Boolean) result.get("success");
                if (Boolean.TRUE.equals(success)) {
                    successCount++;
                    String processStatus = (String) result.get("processStatus");
                    if ("상품용".equals(processStatus)) {
                        productUseCount++;
                    } else if ("이첩".equals(processStatus)) {
                        transferCount++;
                    } else if ("내사종결".equals(processStatus) || "정상".equals(processStatus)) {
                        normalCount++;
                    } else {
                        normalCount++;
                    }
                } else {
                    failCount++;
                }
            }

            Map<String, Object> resultData = new HashMap<>();
            resultData.put("compareResults", compareResults);
            resultData.put("totalCount", targetList.size());
            resultData.put("successCount", successCount);
            resultData.put("failCount", failCount);
            resultData.put("productUseCount", productUseCount);
            resultData.put("transferCount", transferCount);
            resultData.put("normalCount", normalCount);

            log.info("========== 미필 API 호출 및 비교 완료 (병렬처리) ==========");
            log.info("전체: {}건, 성공: {}건, 실패: {}건, 상품용: {}건, 이첩: {}건, 정상: {}건",
                    targetList.size(), successCount, failCount, productUseCount, transferCount, normalCount);

            return resultData;

        } finally {
            // ExecutorService 종료
            executor.shutdown();
            try {
                if (!executor.awaitTermination(120, TimeUnit.SECONDS)) {
                    executor.shutdownNow();
                }
            } catch (InterruptedException e) {
                executor.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * 개별 건을 처리 (개별 트랜잭션)
     *
     * @param target 처리할 대상 데이터
     * @param plusDay 부과일자 가산일
     * @return 처리 결과
     */
    private Map<String, Object> processOneTarget(Map<String, String> target, int plusDay, go.kr.project.login.model.LoginUserVO userInfo) {
        String carFfnlgTrgtIncmpId = target.get("carFfnlgTrgtIncmpId");
        String vhclno = target.get("vhclno");
        String inspVldPrd = target.get("inspVldPrd");

        log.info("처리 중 - 차량번호: {}, 검사유효기간: {}", vhclno, inspVldPrd);

        Map<String, Object> compareResult = new HashMap<>();
        compareResult.put("carFfnlgTrgtIncmpId", carFfnlgTrgtIncmpId);
        compareResult.put("vhclno", vhclno);

        try {
            // 개별 트랜잭션으로 실행
            String statusCode = transactionTemplate.execute(status -> {
                try {
                    // 1. 기존 데이터 조회
                    CarFfnlgTrgtIncmpVO existingData = new CarFfnlgTrgtIncmpVO();
                    existingData.setCarFfnlgTrgtIncmpId(carFfnlgTrgtIncmpId);
                    existingData = mapper.selectOne(existingData);

                    if (existingData == null) {
                        String errorMsg = String.format("기존 데이터를 찾을 수 없습니다. 차량번호: %s", vhclno);
                        log.error(errorMsg);
                        throw new MessageException(errorMsg);
                    }

                    // 2. 처리상태 검증 - 접수상태(01)가 아닌 경우 API 호출 불가
                    if (!TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT.equals(existingData.getTaskPrcsSttsCd())) {
                        String errorMsg = String.format("접수 상태(01)인 데이터만 API 호출이 가능합니다. 차량번호: %s, 현재 상태: %s",
                                vhclno, existingData.getTaskPrcsSttsCd());
                        log.error(errorMsg);
                        throw new MessageException(errorMsg);
                    }

                    // 3. 검사유효기간에서 부과일자 계산 (종료일 + 가산일)
                    String levyCrtrYmd = calculateLevyCrtrYmdFromInspVldPrd(inspVldPrd, plusDay);
                    existingData.setLevyCrtrYmd(levyCrtrYmd);
                    log.info("부과일자 계산 완료 - 검사유효기간: {}, 부과일자: {}", inspVldPrd, levyCrtrYmd);

                    // 4. 비교 로직 실행
                    return comparisonOmService.executeComparison(existingData, userInfo);

                } catch (Exception e) {
                    // 트랜잭션 롤백
                    status.setRollbackOnly();
                    throw e;
                }
            });

            // 결과 처리
            if (statusCode != null) {
                // 비교 규칙이 적용됨
                if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_02_PRODUCT_USE.equals(statusCode)) {
                    compareResult.put("processStatus", "상품용");
                    compareResult.put("message", "상품용으로 처리되었습니다.");
                } else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_03_TRANSFER.equals(statusCode)) {
                    compareResult.put("processStatus", "이첩");
                    compareResult.put("message", "이첩으로 처리되었습니다.");
                } else if (TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_04_INVESTIGATION_CLOSED.equals(statusCode)) {
                    compareResult.put("processStatus", "내사종결");
                    compareResult.put("message", "내사종결로 처리되었습니다.");
                } else {
                    compareResult.put("processStatus", "기타");
                    compareResult.put("message", "기타 상태로 처리되었습니다.");
                }
                compareResult.put("success", true);
            } else {
                // 정상 처리 (비교 로직에 해당 안됨)
                compareResult.put("success", true);
                compareResult.put("message", "정상 처리되었습니다.");
                compareResult.put("processStatus", "정상");
            }

        } catch (Exception e) {
            log.error("데이터 비교 중 오류 발생 - 차량번호: {}", vhclno, e);
            compareResult.put("success", false);
            compareResult.put("message", "처리 실패: " + e.getMessage());
            compareResult.put("processStatus", "실패");
        }

        return compareResult;
    }

    /**
     * 과태료 대상 미필 정보를 일괄 저장
     */
    @Override
    @Transactional
    public int saveCarFfnlgTrgtIncmps(CarFfnlgTrgtIncmpModifiedDataVO modifyData) {
        int result = 0;

        // 1. 삭제된 행 처리
        List<CarFfnlgTrgtIncmpVO> deletedRows = modifyData.getDeletedRows();
        if (deletedRows != null && !deletedRows.isEmpty()) {
            for (CarFfnlgTrgtIncmpVO vo : deletedRows) {
                String dltr = SessionUtil.getUserId();
                if (dltr == null || dltr.isEmpty()) {
                    throw new MessageException("로그인 정보가 없습니다.");
                }
                vo.setDltr(dltr);
                result += mapper.delete(vo);
            }
        }

        // 2. 추가된 행 처리
        List<CarFfnlgTrgtIncmpVO> createdRows = modifyData.getCreatedRows();
        if (createdRows != null && !createdRows.isEmpty()) {
            for (CarFfnlgTrgtIncmpVO vo : createdRows) {
                String rgtr = SessionUtil.getUserId();
                if (rgtr == null || rgtr.isEmpty()) {
                    throw new MessageException("로그인 정보가 없습니다.");
                }
                vo.setRgtr(rgtr);

                if (vo.getTaskPrcsSttsCd() == null || vo.getTaskPrcsSttsCd().isEmpty()) {
                    vo.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT);
                }

                result += mapper.insert(vo);
            }
        }

        // 3. 수정된 행 처리
        List<CarFfnlgTrgtIncmpVO> updatedRows = modifyData.getUpdatedRows();
        if (updatedRows != null && !updatedRows.isEmpty()) {
            for (CarFfnlgTrgtIncmpVO vo : updatedRows) {
                vo.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER_DASH));
                result += mapper.updateTaskPrcsSttsCdAndRmrk(vo);
            }
        }

        log.info("과태료 대상 미필 일괄 저장 완료 - 처리 건수: {}", result);
        return result;
    }

    @Override
    public List<CarFfnlgTrgtIncmpExcelVO> selectListForExcel(CarFfnlgTrgtIncmpVO vo) {
        log.debug("과태료 대상 미필 목록 엑셀 다운로드용 조회 - 검색조건: {}", vo);
        return mapper.selectListForExcel(vo);
    }

    // ================== 내부 유틸 메서드 ==================

    private static String nvl(Object o) {
        if (o == null) return "";
        return o.toString();
    }

    private static String padRightBytes(String s, int byteLen, String encoding) throws Exception {
        if (byteLen <= 0) return nvl(s);
        String v = nvl(s);
        byte[] b = v.getBytes(encoding);
        if (b.length == byteLen) return v;
        if (b.length > byteLen) {
            return truncateToBytes(v, byteLen, encoding);
        }
        StringBuilder sb = new StringBuilder(v);
        while (sb.toString().getBytes(encoding).length < byteLen) {
            sb.append(' ');
        }
        return sb.toString();
    }

    private static String truncateToBytes(String s, int byteLen, String encoding) throws Exception {
        if (s == null) return "";
        byte[] b = s.getBytes(encoding);
        if (b.length <= byteLen) return s;
        byte[] cut = new byte[byteLen];
        System.arraycopy(b, 0, cut, 0, byteLen);
        for (int len = byteLen; len > 0; len--) {
            try {
                return new String(cut, 0, len, encoding);
            } catch (Exception ignore) {
            }
        }
        return "";
    }

    private String buildErrorMessage(List<String> errorMessages) {
        if (errorMessages.isEmpty()) {
            return "파일 업로드 중 오류가 발생했습니다.";
        }

        StringBuilder sb = new StringBuilder();
        sb.append("파일 업로드 실패 - 전체 롤백 처리되었습니다.\n\n");
        sb.append("[오류 상세 내역]\n");

        int displayCount = Math.min(errorMessages.size(), 10);
        for (int i = 0; i < displayCount; i++) {
            sb.append(errorMessages.get(i)).append("\n");
        }

        if (errorMessages.size() > 10) {
            sb.append("... 외 ").append(errorMessages.size() - 10).append("건\n");
        }

        return sb.toString();
    }

    /**
     * 설정 기반 바이트 단위 고정폭 데이터 파싱 (2줄 1세트)
     */
    private CarFfnlgTrgtIncmpVO parseFixedWidthData(String firstLine, String secondLine,
                                                      int dataLineNumber, List<String> errorMessages,
                                                      String prgrmId, String prcsYmd, String otptDt) {
        try {
            CarFfnlgTrgtIncmpVO vo = new CarFfnlgTrgtIncmpVO();

            String encoding = parseConfig.getEncoding();
            log.debug("[데이터 {}] 파싱 시작 - 인코딩: {}, 한글바이트: {}", dataLineNumber, encoding, parseConfig.getHangulByteSize());

            // 첫 번째 줄 파싱
            byte[] firstBytes = firstLine.getBytes(encoding);
            int pos = 0;

            // 디버깅: 첫 30바이트 출력
            log.debug("=== 바이트 디버깅 ===");
            log.debug("firstLine 원본: [{}]", firstLine);
            log.debug("firstBytes 길이: {}", firstBytes.length);
            int debugLen = Math.min(30, firstBytes.length);
            byte[] debugBytes = new byte[debugLen];
            System.arraycopy(firstBytes, 0, debugBytes, 0, debugLen);
            log.debug("첫 30바이트: [{}]", new String(debugBytes, encoding));
            for (int k = 0; k < debugLen; k++) {
                log.debug("  [{}] byte={}, char=[{}]", k, firstBytes[k], new String(new byte[]{firstBytes[k]}, encoding));
            }

            // 번호
            log.debug("parseConfig 정보: encoding={}, hangulByteSize={}", parseConfig.getEncoding(), parseConfig.getHangulByteSize());
            log.debug("byteSize2={}, byteSize3={}", parseConfig.getByteSize2(), parseConfig.getByteSize3());
            if (parseConfig.getByteSize2() != null) {
                log.debug("byteSize2.firstLine={}", parseConfig.getByteSize2().getFirstLine());
            }
            int len = parseConfig.getFirstLineLength("no");
            log.debug("번호 읽기: pos={}, len={}", pos, len);
            String no = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.debug("번호 결과: [{}] (trim 전: [{}])", no, extractByteLength(firstBytes, pos, len, encoding));
            pos += len;

            // 차량번호
            len = parseConfig.getFirstLineLength("vhclno");
            log.debug("차량번호 읽기: pos={}, len={}", pos, len);
            String vhclno = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.debug("차량번호 결과: [{}] (trim 전: [{}])", vhclno, extractByteLength(firstBytes, pos, len, encoding));
            pos += len;

            // 소유자명 (주민번호 빈칸 포함 35바이트)
            len = parseConfig.getFirstLineLength("ownr-nm");
            String ownrNm = extractByteLength(firstBytes, pos, len, encoding).trim();
            pos += len;

            // 자동차명
            len = parseConfig.getFirstLineLength("car-nm");
            String carNm = extractByteLength(firstBytes, pos, len, encoding).trim();
            pos += len;

            // 사용본거지주소
            len = parseConfig.getFirstLineLength("use-strhld-addr");
            String useStrhldAddr = extractByteLength(firstBytes, pos, len, encoding).trim();
            pos += len;

            // 검사유효기간
            len = parseConfig.getFirstLineLength("insp-vld-prd");
            String inspVldPrd = extractByteLength(firstBytes, pos, len, encoding).trim();

            // 두 번째 줄 파싱
            byte[] secondBytes = secondLine.getBytes(encoding);
            pos = 0;

            // 공백 스킵
            len = parseConfig.getSecondLineLength("skip");
            pos += len;

            // 주민등록번호
            len = parseConfig.getSecondLineLength("rrno");
            String rrno = extractByteLength(secondBytes, pos, len, encoding).trim();
            pos += len;

            // 사용본거지주소 상세 (나머지)
            len = parseConfig.getSecondLineLength("use-strhld-addr");
            String useStrhldAddrDtl = extractByteLength(secondBytes, pos, len, encoding).trim();

            // VO 설정
            if (!no.isEmpty()) {
                try {
                    vo.setNo(Integer.parseInt(no));
                } catch (NumberFormatException e) {
                    log.warn("번호 파싱 실패: {}", no);
                }
            }
            vo.setPrgrmId(prgrmId);
            vo.setPrcsYmd(prcsYmd);
            vo.setOtptDt(otptDt);
            vo.setVhclno(vhclno);
            vo.setOwnrNm(ownrNm);
            vo.setRrno(rrno);
            vo.setCarNm(carNm);
            vo.setUseStrhldAddr(useStrhldAddr);
            vo.setUseStrhldAddrDtl(useStrhldAddrDtl);
            vo.setInspVldPrd(inspVldPrd);

            log.debug("[데이터 {}] 파싱 완료", dataLineNumber);

            return vo;

        } catch (Exception e) {
            String errorMsg = String.format("[데이터 %d] 파싱 중 오류 발생 - %s", dataLineNumber, e.getMessage());
            errorMessages.add(errorMsg);
            log.error("데이터 {} 파싱 중 오류", dataLineNumber, e);
            return null;
        }
    }

    private String extractByteLength(byte[] bytes, int pos, int length, String encoding) {
        try {
            if (pos < 0) pos = 0;
            if (pos >= bytes.length) return "";

            int actualLength;
            if (length < 0) {
                actualLength = bytes.length - pos;
            } else {
                actualLength = Math.min(length, bytes.length - pos);
            }

            if (actualLength <= 0) return "";

            byte[] extracted = new byte[actualLength];
            System.arraycopy(bytes, pos, extracted, 0, actualLength);

            return new String(extracted, encoding);

        } catch (Exception e) {
            log.error("바이트 추출 중 오류 발생 - pos: {}, length: {}, encoding: {}", pos, length, encoding, e);
            return "";
        }
    }

    /**
     * 파싱된 데이터 유효성 검증
     */
    private List<String> validateParsedData(int dataLineNumber, CarFfnlgTrgtIncmpVO vo) {
        List<String> errors = new ArrayList<>();

        String vhclno = vo.getVhclno() != null ? vo.getVhclno() : "알 수 없음";

        // 1. 차량번호 검증
        if (vo.getVhclno() == null || vo.getVhclno().isEmpty()) {
            errors.add(String.format("[데이터 %d] 차량번호가 누락되었습니다.", dataLineNumber));
        } else if (vo.getVhclno().length() > 30) {
            errors.add(String.format("[데이터 %d] 차량번호가 너무 깁니다. 차량번호: %s (최대 30자)", dataLineNumber, vo.getVhclno()));
        }

        // 2. 소유자명 검증 (소유자명에 null or 공백 들어올수 있음 - 필수 아님)
        if (vo.getOwnrNm() != null && !vo.getOwnrNm().isEmpty() && vo.getOwnrNm().length() > 75) {
            errors.add(String.format("[데이터 %d] 소유자명이 너무 깁니다. 소유자명: %s (최대 75자), 차량번호: %s",
                    dataLineNumber, vo.getOwnrNm(), vhclno));
        }

        // 3. 주민등록번호 검증
        if (vo.getRrno() == null || vo.getRrno().isEmpty()) {
            errors.add(String.format("[데이터 %d] 주민등록번호가 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno));
        } else if (vo.getRrno().length() > 100) {
            errors.add(String.format("[데이터 %d] 주민등록번호가 너무 깁니다. 주민번호 길이: %d (최대 100자), 차량번호: %s",
                    dataLineNumber, vo.getRrno().length(), vhclno));
        }

        // 4. 검사유효기간 검증
        if (vo.getInspVldPrd() == null || vo.getInspVldPrd().isEmpty()) {
            errors.add(String.format("[데이터 %d] 검사유효기간이 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno));
        }

        return errors;
    }

    /**
     * 검사유효기간에서 부과일자 계산
     * 예: "2023-07-12~2025-07-11" -> 종료일(2025-07-11) + 가산일(146일) = 20251203
     */
    private String calculateLevyCrtrYmdFromInspVldPrd(String inspVldPrd, int plusDay) {
        if (inspVldPrd == null || inspVldPrd.isEmpty()) {
            throw new IllegalArgumentException("검사유효기간이 없습니다.");
        }

        // "2023-07-12~2025-07-11" 형식에서 종료일 추출
        String[] parts = inspVldPrd.split("~");
        if (parts.length != 2) {
            throw new IllegalArgumentException("검사유효기간 형식이 올바르지 않습니다: " + inspVldPrd);
        }

        String endDateStr = parts[1].trim().replace("-", "");

        try {
            LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER);
            LocalDate levyDate = endDate.plusDays(plusDay);
            return levyDate.format(DATE_FORMATTER);
        } catch (DateTimeParseException e) {
            throw new IllegalArgumentException("검사유효기간 종료일 파싱 실패: " + endDateStr, e);
        }
    }
}
