package incheon.ags.dss.util;

import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

/**
 * 'data-load' 프로필용 파일 로더의 공통 로직을 정의한 추상 클래스.
 * 1. CommandLineRunner를 구현하여 앱 시작 시 실행.
 * 2. @Transactional을 통해 전체 파일 처리 과정을 하나의 트랜잭션으로 관리.
 * 3. 템플릿 메서드 패턴:
 * - 공통 실행 흐름 (run, processFiles)은 부모가 정의.
 * - 세부 구현 (parseFile, isTargetFile 등)은 자식 클래스에 위임.
 */
public abstract class AbstractFileLoader implements CommandLineRunner {

    // 각 자식 클래스에서 구현할 Logger
    private final Logger log;

    @Autowired
    protected JdbcTemplate jdbcTemplate;

    public AbstractFileLoader(Logger logger) {
        this.log = logger;
    }

    // --- 자식 클래스가 구현해야 하는 추상 메서드 ---

    /** 로더 이름 (로그용) */
    protected abstract String getLoaderName();
    /** 소스 디렉터리 경로 */
    protected abstract Path getSourceDirectory();
    /** 처리 완료 디렉터리 경로 */
    protected abstract Path getProcessedDirectory();
    /** 대상 파일 필터링 조건 */
    protected abstract boolean isTargetFile(Path path);
    /** 파일 파싱 로직 */
    protected abstract List<Object[]> parseFile(Path path) throws Exception;
    /** DB Insert SQL */
    protected abstract String getInsertSql();
    /** Batch Chunk 크기 */
    protected abstract int getChunkSize();
    /** 처리 완료된 파일(그룹) 이동 로직 */
    protected abstract void moveProcessedFileGroup(Path sourceFile, Path processedDir) throws IOException;

    // --- 공통 실행 로직 (템플릿 메서드) ---

    @Override
    public void run(String... args) throws Exception {
        log.info("==========================================");
        log.info("{} 데이터 로드 프로필 활성화", getLoaderName());
        log.info("대상 디렉터리: {}", getSourceDirectory());
        log.info("완료 폴더: {}", getProcessedDirectory());
        log.info("==========================================");

        try {
            processFiles();
            log.info("==========================================");
            log.info("{} 데이터 로드 작업이 성공적으로 완료되었습니다.", getLoaderName());
            log.info("==========================================");
        } catch (Exception e) {
            log.error("==========================================");
            log.error("{} 데이터 로드 작업 중 심각한 오류 발생. 롤백됩니다.", getLoaderName(), e);
            log.error("오류로 인해 파일이 이동되지 않았습니다. 원인 확인 후 재시도하세요.");
            log.debug("Error Trace", e); // 보안 조치: 상세 에러는 debug 레벨로
            log.error("==========================================");
        }
    }

    /**
     * 파일 처리 및 DB 적재의 메인 로직.
     * 이 메서드 전체가 하나의 트랜잭션으로 묶입니다.
     */
    @Transactional
    public void processFiles() throws IOException {
        Path dirPath = getSourceDirectory();
        Path processedDirPath = getProcessedDirectory();

        if (!Files.exists(dirPath)) {
            log.warn("소스 디렉터리가 존재하지 않습니다: {}", dirPath);
            return;
        }
        if (!Files.exists(processedDirPath)) {
            Files.createDirectories(processedDirPath);
            log.info("처리 완료 폴더 생성: {}", processedDirPath);
        }

        List<Path> filesToMove = new ArrayList<>();

        // 1. 모든 대상 파일 파싱 및 즉시 DB 반영
        try (Stream<Path> paths = Files.walk(dirPath, 1)) { // 1: 하위 디렉터리 제외
            paths
                    .filter(Files::isRegularFile)
                    .filter(this::isTargetFile) // 자식 클래스의 필터 로직 사용
                    .forEach(path -> {
                        log.info("파일 처리 시작: {}", path.getFileName());
                        try {
                            // 1-1. 자식 클래스의 파싱 로직 사용
                            List<Object[]> fileData = parseFile(path);
                            log.info("파일 파싱 완료: {} ({} 건)", path.getFileName(), fileData.size());

                            // 1-2. 파싱된 데이터를 즉시 DB에 배치 삽입
                            if (!fileData.isEmpty()) {
                                insertBatchToDB(fileData);
                            } else {
                                log.info("파일 {} 처리 완료. (데이터 없음)", path.getFileName());
                            }

                            // 1-3. 성공 시 파일 이동 목록에 추가
                            filesToMove.add(path);

                        } catch (Exception e) {
                        	// [보안 조치] 파일명은 남기되, 구체적인 예외 메시지(경로 정보 포함 등)는 은닉
                            log.error("파일 처리 중 치명적 오류 발생 (롤백 진행) - 파일명: {}", path.getFileName());
                            log.debug("File Processing Error Detail", e);
                            throw new RuntimeException("파일 처리 중 오류 발생 (관리자 문의 필요)", e);
                        }
                    });
        }

        log.info("모든 파일의 DB 작업 완료. (총 {}개 파일)", filesToMove.size());

        // 2. DB 트랜잭션이 성공적으로 커밋된 후, 파일 이동
        log.info("DB 작업 성공. 처리된 파일 {}개를 이동합니다...", filesToMove.size());
        int moveCount = 0;
        for (Path fileToMove : filesToMove) {
            try {
                // 2-1. 자식 클래스의 파일 이동 로직 사용
                moveProcessedFileGroup(fileToMove, processedDirPath);
                moveCount++;
            } catch (IOException e) {
            	log.error("처리된 파일 이동 실패 (DB 반영됨/파일 이동 필요) - 파일명: {}", fileToMove.getFileName());
                log.debug("Move Failed", e);
            }
        }
        log.info("{} 개의 파일 그룹 이동 완료.", moveCount);
    }

    /**
     * DB batch insert 공통 처리
     */
    private void insertBatchToDB(List<Object[]> batchArgs) {
        String sql = getInsertSql(); // 자식 클래스의 SQL
        int batchSize = getChunkSize(); // 자식 클래스의 Chunk 크기

        for (int i = 0; i < batchArgs.size(); i += batchSize) {
            List<Object[]> batchList = batchArgs.subList(i, Math.min(i + batchSize, batchArgs.size()));
            jdbcTemplate.batchUpdate(sql, batchList);
            log.info("{} / {} 데이터 DB 반영 완료.", (i + batchList.size()), batchArgs.size());
        }
        log.info("총 {} 건 데이터 DB 반영 완료!", batchArgs.size());
    }

    /**
     * 단일 파일 이동 (기본 이동 로직)
     * @throws IOException
     */
    protected void moveSingleFile(Path sourceFile, Path processedDir) throws IOException {
        Files.move(
                sourceFile,
                processedDir.resolve(sourceFile.getFileName()),
                StandardCopyOption.REPLACE_EXISTING
        );
    }
}