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

import egovframework.constant.TaskPrcsSttsConstants;
import egovframework.exception.MessageException;
import egovframework.util.SessionUtil;
import go.kr.project.api.service.ExternalVehicleApiService;
import go.kr.project.carInspectionPenalty.registration.config.CarFfnlgTxtParseConfig;
import go.kr.project.carInspectionPenalty.registration.mapper.CarFfnlgTrgtMapper;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtExcelVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtModifiedDataVO;
import go.kr.project.carInspectionPenalty.registration.model.CarFfnlgTrgtVO;
import go.kr.project.carInspectionPenalty.registration.service.CarFfnlgTrgtService;
import go.kr.project.carInspectionPenalty.registration.service.ComparisonService;
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 CarFfnlgTrgtServiceImpl extends EgovAbstractServiceImpl implements CarFfnlgTrgtService {

    private final CarFfnlgTrgtMapper mapper;
    private final CarFfnlgTxtParseConfig parseConfig;
    private final ExternalVehicleApiService service;
    private final ComparisonService comparisonService;
    private final TransactionTemplate transactionTemplate;
    
    // 날짜 형식 (YYYYMMDD)
    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(CarFfnlgTrgtVO vo) {
        return mapper.selectListTotalCount(vo);
    }
    
    @Override
    public List<CarFfnlgTrgtVO> selectList(CarFfnlgTrgtVO vo) {
        return mapper.selectList(vo);
    }
    
    @Override
    public CarFfnlgTrgtVO selectOne(CarFfnlgTrgtVO vo) {
        return mapper.selectOne(vo);
    }
    
    @Override
    @Transactional
    public int insert(CarFfnlgTrgtVO vo) {
        return mapper.insert(vo);
    }
    
    @Override
    @Transactional
    public int update(CarFfnlgTrgtVO vo) {
        return mapper.update(vo);
    }
    
    @Override
    @Transactional
    public int delete(CarFfnlgTrgtVO vo) {
        return mapper.delete(vo);
    }
    
    /**
     * TXT 파일 업로드 및 파싱하여 DB 저장
     * 
     * 파일 형식: 고정폭 텍스트 파일 (2줄 1세트)
     * - 헤더: 8, 9번째 라인
     * - 데이터: 11번째 라인부터 시작
     * - 각 데이터는 2줄로 구성
     *   1) 첫 번째 줄: 검사소/검사일자/차량번호/소유자명/주민번호/차명/차종/용도/종료일/일수/과태료
     *   2) 두 번째 줄: 최종등록일/주소/유효기간만료일/매매상품
     * 
     * 주의: 파일 단위로 업로드하므로 한 건이라도 실패하면 전체 롤백 처리됨
     */
    @Override
    @Transactional
    public Map<String, Object> uploadAndParseTxtFile(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(".txt") && !originalFilename.toLowerCase().endsWith(".prn"))) {
                throw new IllegalArgumentException("TXT, PRN 파일만 업로드 가능합니다. 선택된 파일: " + originalFilename);
            }
            
            // 파일 크기 검증 (50MB 제한)
            if (file.getSize() > 50 * 1024 * 1024) {
                throw new IllegalArgumentException("파일 크기는 50MB를 초과할 수 없습니다. 파일 크기: " + (file.getSize() / 1024 / 1024) + "MB");
            }
            
            log.info("TXT 파일 업로드 시작 - 파일명: {}, 크기: {} 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);
                }
            }
            
            // 파일 최소 라인 검증 (헤더 10라인 + 데이터 최소 2라인)
            if (allLines.size() < 12) {
                throw new IllegalArgumentException("파일 형식이 올바르지 않습니다. 최소 12라인 이상이어야 합니다. 현재 라인 수: " + allLines.size());
            }
            
            log.info("파일 읽기 완료 - 총 라인 수: {}", allLines.size());
            
            // 11번째 라인부터 데이터 처리 (인덱스 10부터 시작)
            for (int i = 10; i < allLines.size(); i++) {
                String firstLine = allLines.get(i);
                
                // 빈 줄 스킵
                if (firstLine.trim().isEmpty()) {
                    continue;
                }
                
                // 구분선 스킵
                if (firstLine.trim().startsWith("---")) {
                    continue;
                }
                
                // 다음 줄 확인 (2줄 1세트이므로)
                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++;
                
                // 고정폭 파싱
                CarFfnlgTrgtVO vo = parseFixedWidthData(firstLine, secondLine, dataLineNumber, errorMessages);
                
                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));
                }
                
                // 차량번호 중복 체크
                CarFfnlgTrgtVO checkVO = new CarFfnlgTrgtVO();
                checkVO.setVhclno(vo.getVhclno());
                checkVO.setInspYmd(vo.getInspYmd());
                int duplicateCount = mapper.checkDuplicateVhclno(checkVO);
                if (duplicateCount > 0) {
                    String errorMsg = String.format("[데이터 %d] 중복된 차량번호입니다. 차량번호: %s, 검사일자: %s", 
                            dataLineNumber, vo.getVhclno(), vo.getInspYmd());
                    errorMessages.add(errorMsg);
                    // 중복 체크 실패 시 전체 롤백
                    throw new MessageException(buildErrorMessage(errorMessages));
                }
                
                // 업무 처리 상태 및 등록자 설정
                vo.setTaskPrcsSttsCd(TaskPrcsSttsConstants.TASK_PRCS_STTS_CD_01_RCPT);  // 01=접수
                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);
                    // DB 저장 실패 시 전체 롤백
                    throw new MessageException(buildErrorMessage(errorMessages));
                }
                
                // 2줄 1세트이므로 다음 줄 건너뛰기
                i++;
            }
            
            log.info("TXT 파일 처리 완료 - 성공: {}건", 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("TXT 파일 업로드 중 오류 발생 - 전체 롤백 처리", e);
            throw e; // 트랜잭션 롤백을 위해 예외 재발생
        } catch (Exception e) {
            log.error("TXT 파일 업로드 중 예상치 못한 오류 발생", e);
            errorMessages.add("파일 업로드 중 오류가 발생했습니다: " + e.getMessage());
            throw new MessageException(buildErrorMessage(errorMessages), e);
        }
        
        return result;
    }

    /**
     * 목록을 EUC-KR 텍스트로 생성하여 다운로드용 바이트 배열을 반환
     * 중요: 샘플 파일과 완전히 동일한 고정폭 포맷을 맞추기 위해 각 필드를 바이트 기준으로 패딩/절단 처리함 (한글 2바이트)
     */
    @Override
    public byte[] generateEucKrDownloadBytes(CarFfnlgTrgtVO vo) {
        try {
            // 인코딩 및 바이트 규칙 설정 (기본 EUC-KR, 한글 2바이트)
            final String encoding = parseConfig.getEncoding() == null || parseConfig.getEncoding().trim().isEmpty()
                    ? "EUC-KR" : parseConfig.getEncoding().trim();

            // 1) 데이터 조회 (페이징 비활성화)
            List<CarFfnlgTrgtVO> list = mapper.selectList(vo);

            StringBuilder sb = new StringBuilder();

            // 2) 헤더 구성 (샘플 텍스트와 동일)
            sb.append(padLeftBytes("유효기간경과 과태료부과대상 리스트", 83, encoding)).append("\r\n");
            sb.append(padLeftBytes("------------------------------------", 84, encoding)).append("\r\n");
            sb.append("\r\n");
            sb.append("\r\n");
            sb.append("   * 최종등록일이 검사일자보다 늦는 경우는 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.    (재검여부 = *일수)\r\n");
            sb.append("   * 전출차량( *차번호)인 경우 전출 전의 주소입니다. 소유자 및 사용본거지 주소를 재확인하여 주시기 바랍니다.\r\n");
            sb.append("-------------------------------------------------------------------------------------------------------------------------------------------------\r\n");
            sb.append("검사소    검사일자   자동차번호   소유자명                      주민등록번호   차  명         차  종         용  도     종료일      일수   과태료\r\n");
            sb.append("         최종등록일                           주                    소                                  유효기간만료일 매매상품용\r\n");
            sb.append("-------------------------------------------------------------------------------------------------------------------------------------------------\r\n");

            // 3) 데이터 라인 생성 (각 항목 2줄)
            for (CarFfnlgTrgtVO row : list) {
                // 차량번호 별표 처리
                String vhclno = nvl(row.getVhclno());
                String vhclnoAsterisk = " ";
                if (vhclno.startsWith("*")) {
                    vhclnoAsterisk = "*";
                    vhclno = vhclno.substring(1); // 별표 제거
                }

                // 최종등록일 별표 처리
                String lastRegYmd = nvl(row.getLastRegYmd());
                String lastRegYmdAsterisk = " ";
                if (lastRegYmd.startsWith("*")) {
                    lastRegYmdAsterisk = "*";
                    lastRegYmd = lastRegYmd.substring(1); // 별표 제거
                }

                // 첫째줄: 고정폭 필드들 연결
                String firstLine =
                        padRightBytes(nvl(row.getInspstnCd()), parseConfig.getFirstLineLength("inspstn-cd"), encoding) +
                        padRightBytes(row.getInspYmd(), parseConfig.getFirstLineLength("insp-ymd"), encoding) +
                        padRightBytes(vhclnoAsterisk, parseConfig.getFirstLineLength("vhclno-asterisk"), encoding) +
                        padRightBytes(vhclno, parseConfig.getFirstLineLength("vhclno"), encoding) +
                        padRightBytes(nvl(row.getOwnrNm()), parseConfig.getFirstLineLength("ownr-nm"), encoding) +
                        padRightBytes(nvl(row.getRrno()), parseConfig.getFirstLineLength("rrno"), encoding) +
                        padRightBytes(nvl(row.getCarNm()), parseConfig.getFirstLineLength("car-nm"), encoding) +
                        padRightBytes(nvl(row.getCarKnd()), parseConfig.getFirstLineLength("car-knd"), encoding) +
                        padRightBytes(nvl(row.getCarUsg()), parseConfig.getFirstLineLength("car-usg"), encoding) +
                        padRightBytes(row.getInspEndYmd(), parseConfig.getFirstLineLength("insp-end-ymd"), encoding) +
                        padLeftBytes(nvl(row.getDaycnt()), parseConfig.getFirstLineLength("daycnt"), encoding) +
                        padLeftBytes(formatAmtToManWon(row.getFfnlgAmt()), parseConfig.getFirstLineLength("ffnlg-amt"), encoding);

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

                // 둘째줄: skip + 나머지 필드
                String secondLine =
                        padRightBytes("", parseConfig.getSecondLineLength("skip"), encoding) +
                        padRightBytes(lastRegYmdAsterisk, parseConfig.getSecondLineLength("last-reg-ymd-asterisk"), encoding) +
                        padRightBytes(lastRegYmd, parseConfig.getSecondLineLength("last-reg-ymd"), encoding) +
                        padRightBytes(nvl(row.getAddr()), parseConfig.getSecondLineLength("addr"), encoding) +
                        padRightBytes(row.getVldPrdExpryYmd(), parseConfig.getSecondLineLength("vld-prd-expry-ymd"), encoding) +
                        padRightBytes(nvl(row.getTrdGds()), parseConfig.getSecondLineLength("trd-gds"), encoding);

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

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

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

    /** null 안전 치환 */
    private static String nvl(String s) { return s == null ? "" : s; }

    /**
     * 날짜 포맷 변환: YYYYMMDD → YYYY-MM-DD (입력에 '-'가 이미 있으면 그대로 사용)
     * - 샘플 출력과 동일한 포맷을 위함
     */
    private static String formatYmd(String ymd, boolean withHyphen) {
        if (ymd == null || ymd.trim().isEmpty()) return "";
        String v = ymd.trim();
        if (!withHyphen) return v;
        if (v.contains("-")) return v; // 이미 하이픈 포함
        if (v.length() == 8) {
            return v.substring(0,4) + "-" + v.substring(4,6) + "-" + v.substring(6,8);
        }
        return v;
    }

    /**
     * 과태료 금액을 "만원" 단위로 표현
     * - 입력이 숫자형(원)일 경우: 300000 → 30만원
     * - 이미 "만원" 문자열 포함 시 그대로 사용
     */
    private static String formatAmtToManWon(String amt) {
        String v = nvl(amt).trim();
        if (v.isEmpty()) return "";
        if (v.endsWith("만원")) return v;
        try {
            long won = Long.parseLong(v.replaceAll("[^0-9]", ""));
            long man = Math.round(won / 10000.0);
            return String.valueOf(man) + "만원";
        } catch (NumberFormatException e) {
            return v; // 숫자 변환 실패 시 원문 유지
        }
    }

    /** 주어진 문자열을 지정 바이트 길이로 오른쪽 공백 패딩 (EUC-KR 기준 바이트) */
    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 padLeftBytes(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.insert(0, ' ');
        }
        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) {
                // len 감소
            }
        }
        return "";
    }
    
    /**
     * 오류 메시지 목록을 하나의 문자열로 조합
     */
    private String buildErrorMessage(List<String> errorMessages) {
        if (errorMessages.isEmpty()) {
            return "파일 업로드 중 오류가 발생했습니다.";
        }
        
        StringBuilder sb = new StringBuilder();
        sb.append("파일 업로드 실패 - 전체 롤백 처리되었습니다.\n\n");
        sb.append("[오류 상세 내역]\n");
        
        // 최대 10개의 오류 메시지만 표시
        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세트)
     * 
     * application.yml의 car-ffnlg-txt-parse 설정을 기반으로 동적 파싱
     * - 인코딩: UTF-8, EUC-KR, MS949 등 설정 가능
     * - 한글 바이트 크기: UTF-8=3바이트, EUC-KR/MS949=2바이트
     * - 각 필드별 바이트 길이: yml 설정에서 변경 가능 (길이만 지정, -1=끝까지)
     */
    private CarFfnlgTrgtVO parseFixedWidthData(String firstLine, String secondLine, 
                                                 int dataLineNumber, List<String> errorMessages) {
        try {
            CarFfnlgTrgtVO vo = new CarFfnlgTrgtVO();
            
            // 설정된 인코딩 가져오기
            String encoding = parseConfig.getEncoding();
            log.info("[DEBUG_LOG] ====== 데이터 {} 파싱 시작 (인코딩: {}, 한글바이트: {}) ======", 
                    dataLineNumber, encoding, parseConfig.getHangulByteSize());
            log.info("[DEBUG_LOG] 첫째줄 원본: [{}]", firstLine);
            log.info("[DEBUG_LOG] 둘째줄 원본: [{}]", secondLine);
            
            // 첫 번째 줄 바이트 단위 파싱 (길이 기반)
            byte[] firstBytes = firstLine.getBytes(encoding);
            log.info("[DEBUG_LOG] 첫째줄 바이트 길이: {}", firstBytes.length);
            
            int pos = 0; // 현재 바이트 위치
            
            // 검사소 (8바이트)
            int len = parseConfig.getFirstLineLength("inspstn-cd");
            String inspstnCd = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 검사소(inspstnCd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, inspstnCd);
            pos += len;
            
            // 검사일자 (12바이트)
            len = parseConfig.getFirstLineLength("insp-ymd");
            String inspYmd = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 검사일자(inspYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, inspYmd);
            pos += len;

            // 차량번호 별표 공간 (1바이트)
            len = parseConfig.getFirstLineLength("vhclno-asterisk");
            String vhclnoAsterisk = extractByteLength(firstBytes, pos, len, encoding);
            log.info("[DEBUG_LOG] 차량번호 별표(vhclnoAsterisk) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, vhclnoAsterisk);
            pos += len;

            // 차량번호 (12바이트)
            len = parseConfig.getFirstLineLength("vhclno");
            String vhclno = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 차량번호(vhclno) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, vhclno);
            pos += len;

            // 별표가 있으면 차량번호 앞에 붙임
            if ("*".equals(vhclnoAsterisk.trim())) {
                vhclno = "*" + vhclno;
                log.info("[DEBUG_LOG] 차량번호에 별표 추가: [{}]", vhclno);
            }
            
            // 소유자명 (31바이트)
            len = parseConfig.getFirstLineLength("ownr-nm");
            String ownrNm = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 소유자명(ownrNm) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, ownrNm);
            pos += len;
            
            // 주민번호 (15바이트)
            len = parseConfig.getFirstLineLength("rrno");
            String rrno = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 주민번호(rrno) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, rrno);
            pos += len;
            
            // 차명 (16바이트)
            len = parseConfig.getFirstLineLength("car-nm");
            String carNm = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 차명(carNm) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, carNm);
            pos += len;
            
            // 차종 (25바이트)
            len = parseConfig.getFirstLineLength("car-knd");
            String carKnd = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 차종(carKnd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, carKnd);
            pos += len;
            
            // 용도 (12바이트)
            len = parseConfig.getFirstLineLength("car-usg");
            String carUsg = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 용도(carUsg) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, carUsg);
            pos += len;
            
            // 종료일 (12바이트)
            len = parseConfig.getFirstLineLength("insp-end-ymd");
            String inspEndYmd = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 종료일(inspEndYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, inspEndYmd);
            pos += len;

            // 일수 (별표 포함)
            len = parseConfig.getFirstLineLength("daycnt");
            String daycnt = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 일수(daycnt) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, daycnt);
            pos += len;
            
            // 과태료 (-1=끝까지)
            len = parseConfig.getFirstLineLength("ffnlg-amt");
            String ffnlgAmt = extractByteLength(firstBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 과태료(ffnlgAmt) [{}바이트, 위치 {}~끝] = [{}]", len, pos, ffnlgAmt);
            
            // 두 번째 줄 바이트 단위 파싱 (길이 기반)
            byte[] secondBytes = secondLine.getBytes(encoding);
            log.info("[DEBUG_LOG] 둘째줄 바이트 길이: {}", secondBytes.length);
            
            pos = 0; // 위치 초기화
            
            // 공백 스킵 (8바이트)
            len = parseConfig.getSecondLineLength("skip");
            log.info("[DEBUG_LOG] 공백 스킵 [{}바이트]", len);
            pos += len;

            // 최종등록일 별표 공간 (1바이트)
            len = parseConfig.getSecondLineLength("last-reg-ymd-asterisk");
            String lastRegYmdAsterisk = extractByteLength(secondBytes, pos, len, encoding);
            log.info("[DEBUG_LOG] 최종등록일 별표(lastRegYmdAsterisk) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, lastRegYmdAsterisk);
            pos += len;

            // 최종등록일 (11바이트)
            len = parseConfig.getSecondLineLength("last-reg-ymd");
            String lastRegYmd = extractByteLength(secondBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 최종등록일(lastRegYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, lastRegYmd);
            pos += len;

            // 별표가 있으면 최종등록일 앞에 붙임
            if ("*".equals(lastRegYmdAsterisk.trim())) {
                lastRegYmd = "*" + lastRegYmd;
                log.info("[DEBUG_LOG] 최종등록일에 별표 추가: [{}]", lastRegYmd);
            }
            
            // 주소 (88바이트)
            len = parseConfig.getSecondLineLength("addr");
            String addr = extractByteLength(secondBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 주소(addr) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, addr);
            pos += len;
            
            // 유효기간만료일 (12바이트)
            len = parseConfig.getSecondLineLength("vld-prd-expry-ymd");
            String vldPrdExpryYmd = extractByteLength(secondBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 유효기간만료일(vldPrdExpryYmd) [{}바이트, 위치 {}-{}] = [{}]", len, pos, pos + len, vldPrdExpryYmd);
            pos += len;
            
            // 매매상품 (-1=끝까지)
            len = parseConfig.getSecondLineLength("trd-gds");
            String trdGds = extractByteLength(secondBytes, pos, len, encoding).trim();
            log.info("[DEBUG_LOG] 매매상품(trdGds) [{}바이트, 위치 {}~끝] = [{}]", len, pos, trdGds);
            
            // 날짜에 '*' 특수문자가 붙는 경우때문에
            // 날짜 형식 변환 하지않고 원본 그대로 저장 (YYYY-MM-DD -> YYYYMMDD)
            //inspYmd = convertDateFormat(inspYmd);
            //inspEndYmd = convertDateFormat(inspEndYmd);
            //lastRegYmd = convertDateFormat(lastRegYmd);
            //vldPrdExpryYmd = convertDateFormat(vldPrdExpryYmd);
            
            log.info("[DEBUG_LOG] 날짜 변환 후 - inspYmd: [{}], inspEndYmd: [{}], lastRegYmd: [{}], vldPrdExpryYmd: [{}]",
                    inspYmd, inspEndYmd, lastRegYmd, vldPrdExpryYmd);
            
            // 과태료 금액 숫자만 추출 (예: "30만원" -> "300000")
            ffnlgAmt = extractNumericAmount(ffnlgAmt);
            log.info("[DEBUG_LOG] 과태료 변환 후: [{}]", ffnlgAmt);

            // 재검여부 설정 (일수에 '*'가 있으면 Y, 없으면 N)
            String reinspYn = "N";
            if (daycnt != null && daycnt.contains("*")) {
                reinspYn = "Y";
            }
            log.info("[DEBUG_LOG] 재검여부(reinspYn): [{}] (일수: {})", reinspYn, daycnt);

            // VO 설정
            vo.setInspstnCd(inspstnCd);
            vo.setInspYmd(inspYmd);
            vo.setVhclno(vhclno);
            vo.setOwnrNm(ownrNm);
            vo.setRrno(rrno);
            vo.setCarNm(carNm);
            vo.setCarKnd(carKnd);
            vo.setCarUsg(carUsg);
            vo.setInspEndYmd(inspEndYmd);
            vo.setDaycnt(daycnt);
            vo.setReinspYn(reinspYn);
            vo.setFfnlgAmt(ffnlgAmt);
            vo.setLastRegYmd(lastRegYmd);
            vo.setAddr(addr);
            vo.setVldPrdExpryYmd(vldPrdExpryYmd);
            vo.setTrdGds(trdGds);
            
            log.info("[DEBUG_LOG] ====== 데이터 {} 파싱 완료 ======", dataLineNumber);
            
            return vo;
            
        } catch (Exception e) {
            String errorMsg = String.format("[데이터 %d] 파싱 중 오류 발생 - %s", dataLineNumber, e.getMessage());
            errorMessages.add(errorMsg);
            log.error("데이터 {} 파싱 중 오류 - 첫째줄: {}, 둘째줄: {}", dataLineNumber, firstLine, secondLine, e);
            return null;
        }
    }
    
    /**
     * 바이트 배열에서 지정된 위치부터 지정된 길이만큼 추출하여 문자열로 반환
     * 
     * @param bytes 원본 바이트 배열
     * @param pos 시작 바이트 위치 (포함)
     * @param length 추출할 바이트 길이 (-1=끝까지)
     * @param encoding 인코딩 (UTF-8, EUC-KR, MS949 등)
     * @return 추출된 문자열
     */
    private String extractByteLength(byte[] bytes, int pos, int length, String encoding) {
        try {
            // 위치 검증
            if (pos < 0) pos = 0;
            if (pos >= bytes.length) return "";
            
            // 길이 계산 (-1이면 끝까지)
            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 "";
        }
    }
    
    
    /**
     * 날짜 형식 변환 (YYYY-MM-DD -> YYYYMMDD)
     */
    private String convertDateFormat(String dateStr) {
        if (dateStr == null || dateStr.isEmpty()) {
            return "";
        }
        // 하이픈 제거
        return dateStr.replace("-", "");
    }
    
    /**
     * 과태료 금액에서 숫자만 추출
     * 예: "30만원" -> "300000", "56만원" -> "560000"
     */
    private String extractNumericAmount(String amountStr) {
        if (amountStr == null || amountStr.isEmpty()) {
            return "";
        }
        
        // "만원" 형식 처리
        if (amountStr.contains("만원")) {
            String numStr = amountStr.replace("만원", "").trim();
            try {
                int num = Integer.parseInt(numStr);
                return String.valueOf(num * 10000);
            } catch (NumberFormatException e) {
                log.warn("과태료 금액 파싱 실패: {}", amountStr);
                return amountStr;
            }
        }
        
        // 숫자만 추출
        return amountStr.replaceAll("[^0-9]", "");
    }
    
    /**
     * 파싱된 데이터 유효성 검증
     * 
     * 필수 필드: 검사소, 검사일자, 차량번호, 소유자명, 주민번호, 과태료금액
     * 선택 필드: 차명, 차종, 용도, 검사종료일자, 일수, 최종등록일, 주소, 유효기간만료일, 매매상품
     */
    private List<String> validateParsedData(int dataLineNumber, CarFfnlgTrgtVO vo) {
        List<String> errors = new ArrayList<>();
        
        String vhclno = vo.getVhclno() != null ? vo.getVhclno() : "알 수 없음";
        
        // 1. 검사소 코드 검증
        if (vo.getInspstnCd() == null || vo.getInspstnCd().isEmpty()) {
            errors.add(String.format("[데이터 %d] 검사소 코드가 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno));
        } else if (vo.getInspstnCd().length() > 8) {
            errors.add(String.format("[데이터 %d] 검사소 코드가 너무 깁니다. 검사소: %s (최대 8자), 차량번호: %s", 
                    dataLineNumber, vo.getInspstnCd(), vhclno));
        }
        
        // 2. 검사일자 검증
        if (vo.getInspYmd() == null || vo.getInspYmd().isEmpty()) {
            errors.add(String.format("[데이터 %d] 검사일자가 누락되었습니다. 차량번호: %s", dataLineNumber, vhclno));
        }
        /*else if (!isValidDate(vo.getInspYmd())) {
            errors.add(String.format("[데이터 %d] 검사일자 형식이 올바르지 않습니다. 검사일자: %s (YYYY-MM-DD 형식이어야 함), 차량번호: %s",
                    dataLineNumber, vo.getInspYmd(), vhclno));
        }*/
        
        // 3. 차량번호 검증
        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()));
        } else if (!isValidVehicleNumber(vo.getVhclno())) {
            errors.add(String.format("[데이터 %d] 차량번호 형식이 올바르지 않습니다. 차량번호: %s (예: 12가3456, 123가4567)", 
                    dataLineNumber, vo.getVhclno()));
        }
        
        // 4. 소유자명 검증 (소유자명에 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));
        }
        
        // 5. 주민등록번호 검증
        if (vo.getRrno() == null || vo.getRrno().isEmpty()) {
            errors.add(String.format("[데이터 %d] 주민등록번호가 누락되었습니다. 차량번호: %s, 소유자: %s", 
                    dataLineNumber, vhclno, vo.getOwnrNm()));
        } else if (vo.getRrno().length() > 100) {
            errors.add(String.format("[데이터 %d] 주민등록번호가 너무 깁니다. 주민번호 길이: %d (최대 100자), 차량번호: %s", 
                    dataLineNumber, vo.getRrno().length(), vhclno));
        } else if (!isValidRrno(vo.getRrno())) {
            errors.add(String.format("[데이터 %d] 주민등록번호 형식이 올바르지 않습니다. 주민번호: %s (13자리 숫자), 차량번호: %s", 
                    dataLineNumber, vo.getRrno(), vhclno));
        }
        
        // 6. 과태료금액 검증
        if (vo.getFfnlgAmt() == null || vo.getFfnlgAmt().isEmpty()) {
            errors.add(String.format("[데이터 %d] 과태료금액이 누락되었습니다. 차량번호: %s, 소유자: %s", 
                    dataLineNumber, vhclno, vo.getOwnrNm()));
        } else if (!isNumeric(vo.getFfnlgAmt())) {
            errors.add(String.format("[데이터 %d] 과태료금액은 숫자여야 합니다. 과태료금액: %s, 차량번호: %s", 
                    dataLineNumber, vo.getFfnlgAmt(), vhclno));
        } else if (vo.getFfnlgAmt().length() > 10) {
            errors.add(String.format("[데이터 %d] 과태료금액이 너무 큽니다. 과태료금액: %s (최대 10자리), 차량번호: %s", 
                    dataLineNumber, vo.getFfnlgAmt(), vhclno));
        }
        
        // 7. 선택 필드 길이 검증
        if (vo.getCarNm() != null && vo.getCarNm().length() > 100) {
            errors.add(String.format("[데이터 %d] 자동차명이 너무 깁니다. 자동차명: %s (최대 100자), 차량번호: %s", 
                    dataLineNumber, vo.getCarNm(), vhclno));
        }
        
        if (vo.getCarKnd() != null && vo.getCarKnd().length() > 100) {
            errors.add(String.format("[데이터 %d] 자동차종류가 너무 깁니다. 자동차종류: %s (최대 100자), 차량번호: %s", 
                    dataLineNumber, vo.getCarKnd(), vhclno));
        }
        
        if (vo.getCarUsg() != null && vo.getCarUsg().length() > 100) {
            errors.add(String.format("[데이터 %d] 자동차용도가 너무 깁니다. 자동차용도: %s (최대 100자), 차량번호: %s", 
                    dataLineNumber, vo.getCarUsg(), vhclno));
        }
        
        if (vo.getAddr() != null && vo.getAddr().length() > 600) {
            errors.add(String.format("[데이터 %d] 주소가 너무 깁니다. 주소 길이: %d (최대 600자), 차량번호: %s", 
                    dataLineNumber, vo.getAddr().length(), vhclno));
        }
        
        // 8. 선택 필드 날짜 검증 (값이 있는 경우에만), '*' 특수문자 들어간 경우때문에 일자형식 틀어짐, 제거
        //if (vo.getInspEndYmd() != null && !vo.getInspEndYmd().isEmpty() && !isValidDate(vo.getInspEndYmd())) {
        //    errors.add(String.format("[데이터 %d] 검사종료일자 형식이 올바르지 않습니다. 검사종료일자: %s (YYYYMMDD 형식), 차량번호: %s",
        //            dataLineNumber, vo.getInspEndYmd(), vhclno));
        //}
        //
        //if (vo.getLastRegYmd() != null && !vo.getLastRegYmd().isEmpty() && !isValidDate(vo.getLastRegYmd())) {
        //    errors.add(String.format("[데이터 %d] 최종등록일자 형식이 올바르지 않습니다. 최종등록일자: %s (YYYYMMDD 형식), 차량번호: %s",
        //            dataLineNumber, vo.getLastRegYmd(), vhclno));
        //}
        //
        //if (vo.getVldPrdExpryYmd() != null && !vo.getVldPrdExpryYmd().isEmpty() && !isValidDate(vo.getVldPrdExpryYmd())) {
        //    errors.add(String.format("[데이터 %d] 유효기간만료일자 형식이 올바르지 않습니다. 유효기간만료일자: %s (YYYYMMDD 형식), 차량번호: %s",
        //            dataLineNumber, vo.getVldPrdExpryYmd(), vhclno));
        //}
        
        // 9. 일수 검증 (값이 있는 경우에만)
        // 일수에 '*' 특수문자가 포함될 수 있음 (재검여부 표시용)
        if (vo.getDaycnt() != null && !vo.getDaycnt().isEmpty()) {
            String daycnt = vo.getDaycnt().trim();
            // '*' 제거 후 숫자 검증
            String daycntWithoutAsterisk = daycnt.replace("*", "").trim();
            if (!daycntWithoutAsterisk.isEmpty() && !isNumeric(daycntWithoutAsterisk)) {
                errors.add(String.format("[데이터 %d] 일수는 숫자 또는 '*숫자' 형식이어야 합니다. 일수: %s, 차량번호: %s",
                        dataLineNumber, vo.getDaycnt(), vhclno));
            }
        }
        
        return errors;
    }
    
    
    /**
     * 날짜 유효성 검증 (YYYYMMDD 형식)
     * @param dateStr 날짜 문자열
     * @return 유효성 여부
     */
    private boolean isValidDate(String dateStr) {
        if (dateStr == null || dateStr.length() != 8) {
            return false;
        }
        
        try {
            LocalDate.parse(dateStr, DATE_FORMATTER);
            return true;
        } catch (DateTimeParseException e) {
            return false;
        }
    }
    
    /**
     * 숫자 여부 검증
     * @param str 문자열
     * @return 숫자 여부
     */
    private boolean isNumeric(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }
        
        try {
            Long.parseLong(str);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
    
    /**
     * 차량번호 형식 검증
     *
     * 유효한 형식:
     * - 일반: 12가3456, 123가4567 (2~3자리 숫자 + 한글 + 4자리 숫자)
     * - 특수: 경기11사2222 (지역명 + 숫자 + 한글 + 숫자)
     * - '*' 특수문자 포함 가능 (전출차량 등 특정 상황 표시)
     *
     * @param vhclno 차량번호
     * @return 유효성 여부
     */
    private boolean isValidVehicleNumber(String vhclno) {
        if (vhclno == null || vhclno.isEmpty()) {
            return false;
        }

        // 공백 제거
        vhclno = vhclno.trim();

        // '*' 특수문자를 임시로 제거하여 검증 (전출차량 등 특정 상황 표시용)
        String vhclnoWithoutAsterisk = vhclno.replace("*", "");

        // '*'만 있는 경우는 제외
        if (vhclnoWithoutAsterisk.isEmpty()) {
            return false;
        }

        // 최소 길이 체크 (예: 12가3456 = 7자, *12가3456 = 8자)
        if (vhclnoWithoutAsterisk.length() < 7) {
            return false;
        }

        // 패턴1: 일반 차량번호 (2~3자리 숫자 + 한글 1자 + 4자리 숫자)
        // 예: 12가3456, 123가4567, *12가3456
        if (vhclnoWithoutAsterisk.matches("\\d{2,3}[가-힣]\\d{4}")) {
            return true;
        }

        // 패턴2: 지역명이 포함된 차량번호 (한글 + 숫자 + 한글 + 숫자)
        // 예: 경기11사2222, 서울12가3456, *경기11사2222
        if (vhclnoWithoutAsterisk.matches("[가-힣]+\\d+[가-힣]\\d+")) {
            return true;
        }

        // 패턴3: 특수 차량번호 (숫자 + 한글 + 숫자)
        // 예: 22고2222, 33마3333, *22고2222
        if (vhclnoWithoutAsterisk.matches("\\d+[가-힣]+\\d+")) {
            return true;
        }

        // 그 외의 경우는 일단 허용 (실제 차량번호 형식이 다양할 수 있음)
        // 최소한 한글과 숫자가 모두 포함되어 있는지 확인
        return vhclnoWithoutAsterisk.matches(".*[가-힣].*") && vhclnoWithoutAsterisk.matches(".*\\d.*");
    }
    
    /**
     * 주민등록번호 형식 검증
     * 
     * 유효한 형식:
     * - 13자리 숫자 (예: 1234567890123)
     * - 6자리-7자리 형식도 허용 (예: 123456-1234567)
     * 
     * @param rrno 주민등록번호
     * @return 유효성 여부
     */
    private boolean isValidRrno(String rrno) {
        if (rrno == null || rrno.isEmpty()) {
            return false;
        }
        
        // 공백 제거
        rrno = rrno.trim();
        
        // 하이픈 제거
        String rrnoDigits = rrno.replace("-", "");
        
        // 13자리 숫자인지 확인
        if (rrnoDigits.length() == 13 && rrnoDigits.matches("\\d{13}")) {
            return true;
        }
        
        // 10자리 이상 숫자로 구성된 경우도 허용 (사업자등록번호 등)
        if (rrnoDigits.length() >= 10 && rrnoDigits.matches("\\d+")) {
            return true;
        }
        
        return false;
    }

    /**
     * 선택된 목록에 대해 API 호출 및 기본정보/등록원부 비교 (병렬처리)
     * 변경사항: 각 건을 병렬로 처리하며, 개별 트랜잭션으로 관리됩니다.
     * 실패한 건만 실패 처리되고, 성공한 건은 정상 처리됩니다.
     *
     * @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("선택된 데이터가 없습니다.");
        }

        // 세션에서 사용자 정보 조회 (병렬처리 전에 메인 스레드에서 조회)
        go.kr.project.login.model.LoginUserVO userInfo = null;
        try {
            userInfo = 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, 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 처리할 대상 데이터
     * @return 처리 결과
     */
    private Map<String, Object> processOneTarget(Map<String, String> target, go.kr.project.login.model.LoginUserVO userInfo) {
        String carFfnlgTrgtId = target.get("carFfnlgTrgtId");
        String vhclno = target.get("vhclno");
        String inspYmd = target.get("inspYmd");

        log.info("처리 중 - 차량번호: {}, 검사일자: {}", vhclno, inspYmd);

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

        try {
            // 개별 트랜잭션으로 실행
            String statusCode = transactionTemplate.execute(status -> {
                try {
                    // 1. 기존 데이터 조회
                    CarFfnlgTrgtVO existingData = new CarFfnlgTrgtVO();
                    existingData.setCarFfnlgTrgtId(carFfnlgTrgtId);
                    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. 비교 로직 실행
                    return comparisonService.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;
    }

    /**
     * 과태료 대상 정보를 일괄 저장
     * 생성, 수정, 삭제된 데이터를 처리합니다.
     *
     * @param modifyData 생성/수정/삭제할 데이터를 담은 VO 객체
     * @return 저장 건수
     */
    @Override
    @Transactional
    public int saveCarFfnlgTrgts(CarFfnlgTrgtModifiedDataVO modifyData) {
        int result = 0;

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

                // 논리 삭제 처리
                result += mapper.delete(vo);
            }
        }

        // 2. 추가된 행 처리
        List<CarFfnlgTrgtVO> createdRows = modifyData.getCreatedRows();
        if (createdRows != null && !createdRows.isEmpty()) {
            for (CarFfnlgTrgtVO 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<CarFfnlgTrgtVO> updatedRows = modifyData.getUpdatedRows();
        if (updatedRows != null && !updatedRows.isEmpty()) {
            for (CarFfnlgTrgtVO vo : updatedRows) {
                vo.setTaskPrcsYmd(LocalDate.now().format(DATE_FORMATTER_DASH));
                result += mapper.updateTaskPrcsSttsCdAndRmrk(vo);
            }
        }

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

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