package incheon.com.file.service.impl;

import de.huxhorn.sulky.ulid.ULID;
import incheon.com.cmm.exception.BusinessException;
import incheon.com.file.mapper.ComFileMapper;
import incheon.com.file.service.ComFileService;
import incheon.com.file.vo.ComFileDtlVO;
import incheon.com.file.vo.ComFileSearchVO;
import incheon.com.file.vo.ComFileVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 공통 파일 서비스 구현체
 * *
 * @since 2025.10.17
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ComFileServiceImpl extends EgovAbstractServiceImpl implements ComFileService {

    private final ComFileMapper comFileMapper;
    private final Environment environment;

    private static final ULID ulid = new ULID();

    @Value("${Globals.comfile.storage.path:../upload}")
    private String uploadPath;

    @Value("${Globals.comfile.storage.temp.path:../upload/temp}")
    private String tempPath;

    // 실제 절대 경로를 저장할 변수
    private Path absoluteUploadPath;
    private Path absoluteTempPath;

    @Value("${Globals.comfile.max.file.size:52428800}")  // 50MB
    private long maxFileSize;

    // Globals.fileUpload.Extensions 사용 (EgovMultipartResolver와 통일)
    @Value("${Globals.fileUpload.Extensions:.hwp.hwpx.doc.xls.xlsx.docx.ppt.pptx.pdf.bmp.gif.jpg.jpeg.png.zip.tiff.tif.dwg.dxf}")
    private String allowedExtensions;

    /**
     * 파일 저장 디렉토리 초기화
     */
    @PostConstruct
    public void init() {
        try {
            // 현재 작업 디렉토리 확인
            String currentDir = System.getProperty("user.dir");
            log.info("현재 작업 디렉토리: {}", currentDir);
            log.info("Active Profile: {}", String.join(",", environment.getActiveProfiles()));

            // 상대 경로를 절대 경로로 변환
            absoluteUploadPath = Paths.get(uploadPath).toAbsolutePath().normalize();
            absoluteTempPath = Paths.get(tempPath).toAbsolutePath().normalize();

            // 업로드 디렉토리 생성
            if (!Files.exists(absoluteUploadPath)) {
                Files.createDirectories(absoluteUploadPath);
                log.info("업로드 디렉토리 생성: {}", absoluteUploadPath);
            }

            // 임시 디렉토리 생성
            if (!Files.exists(absoluteTempPath)) {
                Files.createDirectories(absoluteTempPath);
                log.info("임시 디렉토리 생성: {}", absoluteTempPath);
            }

            log.info("파일 업로드 경로 초기화 완료");
            log.info("- 업로드 경로: {} -> {}", uploadPath, absoluteUploadPath);
            log.info("- 임시 경로: {} -> {}", tempPath, absoluteTempPath);

        } catch (IOException e) {
            log.error("파일 업로드 디렉토리 초기화 실패", e);
            log.error("uploadPath: {}, tempPath: {}", uploadPath, tempPath);
        }
    }

    @Override
    public List<ComFileVO> selectComFileList(ComFileSearchVO searchVO) {
        return comFileMapper.selectComFileList(searchVO);
    }

    @Override
    public int selectComFileListTotCnt(ComFileSearchVO searchVO) {
        return comFileMapper.selectComFileListTotCnt(searchVO);
    }

    @Override
    public ComFileVO selectComFileDetail(String atchFileId) {
        return comFileMapper.selectComFileDetail(atchFileId);
    }

    @Override
    public List<ComFileDtlVO> selectComFileDtlList(String atchFileId) {
        if (!StringUtils.hasText(atchFileId)) {
            return List.of();
        }
        return comFileMapper.selectComFileDtlList(atchFileId);
    }

    @Override
    public ComFileDtlVO selectComFileDtlDetail(String fileId) {
        return comFileMapper.selectComFileDtlDetail(fileId);
    }

    @Override
    public String uploadFiles(MultipartFile[] files, String atchFileId) throws IOException {
        // 파일그룹ID가 없으면 신규 생성
        if (!StringUtils.hasText(atchFileId)) {
            atchFileId = generateUlid();

            // 파일 그룹 생성 (임시)
            ComFileVO comFileVO = new ComFileVO();
            comFileVO.setAtchFileId(atchFileId);
            comFileVO.setTmprYn("Y");  // 임시 파일
            // frstRegId, lastMdfcnId는 MybatisUserSessionInterceptor가 자동 주입

            comFileMapper.insertComFile(comFileVO);
        }

        // 각 파일 업로드
        for (MultipartFile file : files) {
            if (!file.isEmpty()) {
                uploadFile(file, atchFileId);
            }
        }

        return atchFileId;
    }

    @Override
    public ComFileDtlVO uploadFile(MultipartFile file, String atchFileId) throws IOException {
        // 파일 검증
        validateFile(file);

        // 파일그룹ID가 없으면 신규 생성
        if (!StringUtils.hasText(atchFileId)) {
            atchFileId = generateUlid();

            // 파일 그룹 생성 (임시)
            ComFileVO comFileVO = new ComFileVO();
            comFileVO.setAtchFileId(atchFileId);
            comFileVO.setTmprYn("Y");  // 임시 파일
            // frstRegId, lastMdfcnId는 MybatisUserSessionInterceptor가 자동 주입

            comFileMapper.insertComFile(comFileVO);
        }

        // 파일 정보 생성
        ComFileDtlVO fileDtlVO = new ComFileDtlVO();
        fileDtlVO.setFileId(generateUlid());
        fileDtlVO.setAtchFileId(atchFileId);
        fileDtlVO.setOrgnlFileNm(file.getOriginalFilename());
        fileDtlVO.setFileSz(file.getSize());
        fileDtlVO.setMimeTypeNm(file.getContentType());

        // 정렬 순서 설정
        Integer nextSeq = comFileMapper.selectNextFileSortSeq(atchFileId);
        fileDtlVO.setFileSortSeq(nextSeq != null ? nextSeq : 1);

        // 저장 경로 생성 (임시: ../upload/temp/YYYYMMDD/)
        String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        Path absoluteDirPath = absoluteTempPath.resolve(datePath);

        // 디렉토리 생성
        if (!Files.exists(absoluteDirPath)) {
            Files.createDirectories(absoluteDirPath);
            log.debug("임시 디렉토리 생성: {}", absoluteDirPath);
        }

        // 저장 파일명 생성 (TEMP_ULID.확장자)
        String extension = getFileExtension(file.getOriginalFilename());
        String saveFileName = "TEMP_" + fileDtlVO.getFileId() +
                              (StringUtils.hasText(extension) ? "." + extension : "");

        // 파일 저장
        Path filePath = absoluteDirPath.resolve(saveFileName);
        file.transferTo(filePath.toFile());

        // 파일 정보 설정 (상대 경로만 저장)
        String relativePath = "temp/" + datePath;  // 상대 경로: temp/YYYYMMDD
        fileDtlVO.setStrgPathNm(relativePath);
        fileDtlVO.setStrgFileNm(saveFileName);
        // frstRegId, lastMdfcnId는 MybatisUserSessionInterceptor가 자동 주입

        // DB 저장
        comFileMapper.insertComFileDtl(fileDtlVO);

        log.info("파일 업로드 완료: {}", fileDtlVO.getFileId());

        return fileDtlVO;
    }

    @Override
    public void confirmFiles(String atchFileId, String trgtNm) {
        if (!StringUtils.hasText(atchFileId)) {
            return;
        }

        // 파일 그룹 조회
        ComFileVO fileGroup = comFileMapper.selectComFileDetail(atchFileId);
        if (fileGroup == null) {
            throw new BusinessException("파일 그룹을 찾을 수 없습니다: " + atchFileId);
        }

        // 이미 정식 파일이면 무시
        if ("N".equals(fileGroup.getTmprYn())) {
            log.debug("이미 정식 파일입니다: {}", atchFileId);
            return;
        }

        // 임시→정식 전환을 위한 VO 설정
        ComFileVO updateVO = new ComFileVO();
        updateVO.setAtchFileId(atchFileId);
        updateVO.setTrgtNm(trgtNm);
        comFileMapper.updateComFileConfirm(updateVO);

        // 파일들을 정식 경로로 이동
        moveFilesToPermanent(atchFileId);

        log.info("파일 그룹 정식 전환 완료: {} -> {}", atchFileId, trgtNm);
    }

    /**
     * 파일을 임시 경로에서 정식 경로로 이동
     */
    private void moveFilesToPermanent(String atchFileId) {
        List<ComFileDtlVO> files = comFileMapper.selectComFileDtlList(atchFileId);

        for (ComFileDtlVO file : files) {
            try {
                // 현재 경로 (상대 경로를 절대 경로로 변환)
                Path currentAbsolutePath = absoluteUploadPath.resolve(file.getStrgPathNm())
                                                             .resolve(file.getStrgFileNm());

                // 새 경로 생성 (../upload/YYYY/MM/)
                String year = String.valueOf(LocalDate.now().getYear());
                String month = String.format("%02d", LocalDate.now().getMonthValue());
                Path newAbsoluteDirPath = absoluteUploadPath.resolve(year).resolve(month);

                // 디렉토리 생성
                if (!Files.exists(newAbsoluteDirPath)) {
                    Files.createDirectories(newAbsoluteDirPath);
                }

                // 새 파일명 (ULID.확장자)
                String extension = file.getFileExtension();
                String newFileName = file.getFileId() +
                                   (StringUtils.hasText(extension) ? "." + extension : "");

                // 파일 이동
                Path newAbsolutePath = newAbsoluteDirPath.resolve(newFileName);
                Files.move(currentAbsolutePath, newAbsolutePath);

                // DB 업데이트 (경로만 업데이트)
                String relativePath = year + "/" + month;  // 상대 경로: YYYY/MM
                ComFileDtlVO updateVO = new ComFileDtlVO();
                updateVO.setFileId(file.getFileId());
                updateVO.setStrgPathNm(relativePath);
                updateVO.setStrgFileNm(newFileName);
                comFileMapper.updateComFileDtlPath(updateVO);

                log.debug("파일 이동 완료: {} -> {}", currentAbsolutePath, newAbsolutePath);

            } catch (IOException e) {
                log.error("파일 이동 실패: {}", file.getFileId(), e);
            }
        }
    }

    @Override
    public void deleteFile(String fileId) {
        // 파일 정보 조회
        ComFileDtlVO file = comFileMapper.selectComFileDtlDetail(fileId);
        if (file == null) {
            throw new BusinessException("파일을 찾을 수 없습니다: " + fileId);
        }

        // 논리 삭제 표시
        ComFileDtlVO updateVO = new ComFileDtlVO();
        updateVO.setFileId(fileId);
        comFileMapper.updateComFileDtlDelNeed(updateVO);

        log.info("파일 삭제 표시: {}", fileId);
    }

    @Override
    public void deleteFileGroup(String atchFileId) {
        // 파일 목록 조회
        List<ComFileDtlVO> files = comFileMapper.selectComFileDtlList(atchFileId);

        // 물리 파일 삭제
        for (ComFileDtlVO file : files) {
            try {
                Path filePath = getAbsoluteFilePath(file);
                if (Files.exists(filePath)) {
                    Files.delete(filePath);
                    log.debug("물리 파일 삭제: {}", filePath);
                }
            } catch (IOException e) {
                log.error("물리 파일 삭제 실패: {}", file.getFileId(), e);
            }
        }

        // DB 삭제 (CASCADE로 상세도 함께 삭제됨)
        comFileMapper.deleteComFile(atchFileId);

        log.info("파일 그룹 삭제 완료: {}", atchFileId);
    }

    @Override
    public void downloadFile(String fileId, HttpServletResponse response) throws IOException {
        // 파일 정보 조회
        ComFileDtlVO file = comFileMapper.selectComFileDtlDetail(fileId);
        if (file == null) {
            throw new BusinessException("파일을 찾을 수 없습니다: " + fileId);
        }

        // 물리 파일 확인
        Path filePath = getAbsoluteFilePath(file);
        File downloadFile = filePath.toFile();
        if (!downloadFile.exists()) {
            throw new BusinessException("파일이 존재하지 않습니다: " + file.getOrgnlFileNm());
        }

        // 응답 헤더 설정
        response.setContentType(file.getMimeTypeNm() != null ? file.getMimeTypeNm() : "application/octet-stream");
        response.setContentLengthLong(file.getFileSz());

        // 파일명 인코딩
        String encodedFileName = URLEncoder.encode(file.getOrgnlFileNm(), StandardCharsets.UTF_8)
                .replaceAll("\\+", "%20");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
        response.setHeader("Content-Transfer-Encoding", "binary");

        // 파일 전송
        try (FileInputStream fis = new FileInputStream(downloadFile)) {
            FileCopyUtils.copy(fis, response.getOutputStream());
        }

        log.info("파일 다운로드: {} - {}", fileId, file.getOrgnlFileNm());
    }

    @Override
    public void downloadFileGroup(String atchFileId, HttpServletResponse response) throws IOException {
        // 1. 파일 목록 조회
        List<ComFileDtlVO> files = comFileMapper.selectComFileDtlList(atchFileId);

        if (files == null || files.isEmpty()) {
            throw new BusinessException("다운로드할 파일이 없습니다: " + atchFileId);
        }

        // 2. 파일이 1개만 있으면 ZIP으로 압축하지 않고 그대로 다운로드
        if (files.size() == 1) {
            ComFileDtlVO singleFile = files.get(0);
            downloadFile(singleFile.getFileId(), response);
            log.info("파일 그룹 단일 파일 다운로드: {} - {}", atchFileId, singleFile.getOrgnlFileNm());
            return;
        }

        // 3. 파일이 2개 이상이면 ZIP으로 압축하여 다운로드
        // 임시 ZIP 파일 경로 생성 (../upload/temp/zip/YYYYMMDD/)
        String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        Path zipTempDir = absoluteTempPath.resolve("zip").resolve(datePath);

        // 디렉토리 생성
        if (!Files.exists(zipTempDir)) {
            Files.createDirectories(zipTempDir);
            log.debug("ZIP 임시 디렉토리 생성: {}", zipTempDir);
        }

        // 4. 임시 ZIP 파일명 (TEMP_ZIP_{ULID}.zip)
        String tempZipFileName = "TEMP_ZIP_" + generateUlid() + ".zip";
        Path tempZipPath = zipTempDir.resolve(tempZipFileName);

        try {
            // 5. ZIP 파일 생성
            try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(tempZipPath))) {
                for (ComFileDtlVO file : files) {
                    Path filePath = getAbsoluteFilePath(file);

                    // 파일 존재 여부 확인
                    if (!Files.exists(filePath)) {
                        log.warn("파일이 존재하지 않아 스킵합니다: {}", file.getOrgnlFileNm());
                        continue;
                    }

                    // ZIP 엔트리 추가 (원본 파일명 사용)
                    ZipEntry zipEntry = new ZipEntry(file.getOrgnlFileNm());
                    zipEntry.setTime(System.currentTimeMillis());
                    zos.putNextEntry(zipEntry);

                    // 파일 복사
                    Files.copy(filePath, zos);
                    zos.closeEntry();
                }
            }

            // 6. 다운로드 응답 헤더 설정
            String downloadFileName = "files_" + atchFileId + ".zip";
            String encodedFileName = URLEncoder.encode(downloadFileName, StandardCharsets.UTF_8)
                    .replaceAll("\\+", "%20");

            response.setContentType("application/zip");
            response.setContentLengthLong(Files.size(tempZipPath));
            response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"");
            response.setHeader("Content-Transfer-Encoding", "binary");

            // 7. ZIP 파일 전송
            try (FileInputStream fis = new FileInputStream(tempZipPath.toFile())) {
                FileCopyUtils.copy(fis, response.getOutputStream());
            }

            log.info("파일 그룹 ZIP 다운로드 완료: {} ({}개 파일)", atchFileId, files.size());

        } finally {
            // 8. 임시 ZIP 파일 삭제 (다운로드 완료 후)
            try {
                if (Files.exists(tempZipPath)) {
                    Files.delete(tempZipPath);
                    log.debug("임시 ZIP 파일 삭제: {}", tempZipPath);
                }
            } catch (IOException e) {
                log.error("임시 ZIP 파일 삭제 실패: {}", tempZipPath, e);
            }
        }
    }

    @Override
    public void updateFileSortSeq(String fileId, Integer sortSeq) {
        ComFileDtlVO updateVO = new ComFileDtlVO();
        updateVO.setFileId(fileId);
        updateVO.setFileSortSeq(sortSeq);
        comFileMapper.updateComFileDtlSortSeq(updateVO);
    }

    @Override
    public String formatFileSize(Long bytes) {
        if (bytes == null || bytes == 0) {
            return "0 B";
        }

        String[] units = {"B", "KB", "MB", "GB"};
        int unitIndex = 0;
        double size = bytes.doubleValue();

        while (size >= 1024 && unitIndex < units.length - 1) {
            size /= 1024;
            unitIndex++;
        }

        return String.format("%.1f %s", size, units[unitIndex]);
    }

    @Override
    public String getMimeType(String fileName) {
        if (!StringUtils.hasText(fileName)) {
            return "application/octet-stream";
        }

        String extension = getFileExtension(fileName).toLowerCase();

        return switch (extension) {
            case "pdf" -> "application/pdf";
            case "doc", "docx" -> "application/msword";
            case "xls", "xlsx" -> "application/vnd.ms-excel";
            case "ppt", "pptx" -> "application/vnd.ms-powerpoint";
            case "hwp" -> "application/x-hwp";
            case "txt", "sam" -> "text/plain";
            case "jpg", "jpeg" -> "image/jpeg";
            case "png" -> "image/png";
            case "gif" -> "image/gif";
            case "zip" -> "application/zip";
            case "rar" -> "application/x-rar-compressed";
            default -> "application/octet-stream";
        };
    }

    @Override
    public String generateUlid() {
        return ulid.nextULID();
    }

    /**
     * 파일 검증
     */
    private void validateFile(MultipartFile file) {
        if (file.isEmpty()) {
            throw new BusinessException("빈 파일입니다");
        }

        // 파일 크기 검증
        if (file.getSize() > maxFileSize) {
            throw new BusinessException("파일 크기가 초과되었습니다. 최대: " + formatFileSize(maxFileSize));
        }

        // 확장자 검증 (Globals.fileUpload.Extensions 형식: .hwp.hwpx.doc.xls...)
        if (StringUtils.hasText(allowedExtensions)) {
            String extension = getFileExtension(file.getOriginalFilename()).toLowerCase();
            // ".hwp.doc.xls" 형식을 ".확장자." 패턴으로 검색
            String extensionPattern = "." + extension + ".";
            String normalizedAllowed = (allowedExtensions + ".").toLowerCase();

            if (!normalizedAllowed.contains(extensionPattern)) {
                throw new BusinessException("허용되지 않은 파일 확장자입니다: " + extension);
            }
        }
    }

    /**
     * 파일 확장자 추출
     */
    private String getFileExtension(String fileName) {
        if (StringUtils.hasText(fileName) && fileName.contains(".")) {
            return fileName.substring(fileName.lastIndexOf(".") + 1);
        }
        return "";
    }

    /**
     * 상대 경로를 절대 경로로 변환
     * DB에 저장된 상대 경로를 실제 파일 시스템의 절대 경로로 변환
     */
    private Path getAbsoluteFilePath(ComFileDtlVO file) {
        // 상대 경로를 절대 경로로 변환
        Path basePath = absoluteUploadPath;
        Path relativePath = Paths.get(file.getStrgPathNm()).resolve(file.getStrgFileNm());
        return basePath.resolve(relativePath);
    }
}