package com.kamco.cd.training.upload.service;

import com.kamco.cd.training.common.utils.FIleChecker;
import com.kamco.cd.training.postgres.core.UploadSessionCoreService;
import com.kamco.cd.training.upload.dto.UploadDto;
import com.kamco.cd.training.upload.dto.UploadDto.DmlReturn;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
@Service
@RequiredArgsConstructor
public class UploadService {

  private final UploadSessionCoreService uploadSessionCoreService;

  @Value("${file.dataset-dir}")
  private String datasetDir;

  @Value("${file.dataset-tmp-dir}")
  private String datasetTmpDir;

  @Value("${file.dataset-cls-dir}")
  private String datasetClsDir;

  @Value("${file.dataset-cls-tmp-dir}")
  private String datasetClsTmpDir;

  // TODO 미사용시작
  @Transactional
  public DmlReturn initUpload(UploadDto.InitReq initReq) {

    return new DmlReturn("success", "UPLOAD CHUNK INIT");
  }

  // TODO 미사용 끝

  @Transactional
  public UploadDto.UploadRes uploadChunk(UploadDto.UploadAddReq upAddReqDto, MultipartFile file) {

    UploadDto.UploadRes upRes = new UploadDto.UploadRes();

    //    long datasetId = upAddReqDto.getDatasetId();
    long datasetId = 0;
    String uploadId = System.currentTimeMillis() + "";
    //    UUID uuid = UUID.randomUUID();
    UUID uuid = upAddReqDto.getUuid();
    String tmpDataSetDir = "";
    String fianlDir = "";
    String uploadDivi = upAddReqDto.getUploadDivi();
    // String fileName = file.getOriginalFilename();
    String fileName = upAddReqDto.getFileName();
    Integer chunkIndex = upAddReqDto.getChunkIndex();
    Integer chunkTotalIndex = upAddReqDto.getChunkTotalIndex();
    String status = "UPLOADING";

    if (uploadDivi.equals("dataset")) { // 탐지데이터셋
      tmpDataSetDir = datasetTmpDir;
      fianlDir = datasetDir;
    } else if (uploadDivi.equals("cls-dataset")) { // 분류데이터셋
      tmpDataSetDir = datasetClsTmpDir;
      fianlDir = datasetClsDir;
    }

    // 리턴용 파일 값
    upRes.setFilePath(fianlDir);
    upRes.setFileName(fileName);

    upAddReqDto.setUuid(uuid);
    upAddReqDto.setUploadId(uploadId);
    upAddReqDto.setStatus(status);
    upAddReqDto.setFileName(fileName);
    upAddReqDto.setTempPath(tmpDataSetDir);
    upAddReqDto.setFinalPath(fianlDir);

    // 세션 신규,중복체크(초기화 포함)
    UploadDto.uploadDto dto = this.checkUploadSession(upAddReqDto, upRes);
    if (!upRes.getRes().equals("success")) return upRes;

    if (dto != null) {
      tmpDataSetDir = dto.getTempPath();
      fianlDir = dto.getFinalPath();
    }

    // 폴더 생성 및 체크
    if (!checkChunkFoler(upRes, tmpDataSetDir, fianlDir)) return upRes;

    // chunk저장하기
    if (!FIleChecker.multipartChunkSaveTo(file, tmpDataSetDir, chunkIndex)) {
      upRes.setRes("fail");
      upRes.setResMsg("chunkIndex:" + chunkIndex + " 업로드 애러");
    }

    // chunk완료시 merge 및 폴더에 저장
    if (Objects.equals(chunkIndex, chunkTotalIndex)) {

      upAddReqDto.setUploadId(dto != null ? dto.getUploadId() : upAddReqDto.getUploadId());
      upAddReqDto.setStatus("MERGING");
      uploadSessionCoreService.updateUploadSessionStatus(upAddReqDto);

      try {
        this.mergeChunks(tmpDataSetDir, fianlDir, fileName, chunkTotalIndex);
      } catch (IOException e) {
        // throw new RuntimeException(e);
        upRes.setRes("fail");
        upRes.setResMsg("파일 저장 완료(merge) 애러");
        return upRes;
      }

      upAddReqDto.setUploadId(dto != null ? dto.getUploadId() : upAddReqDto.getUploadId());
      upAddReqDto.setStatus("COMPLETE");
      uploadSessionCoreService.updateUploadSessionStatus(upAddReqDto);
    }

    return upRes;
  }

  @Transactional
  public DmlReturn completeUpload(String uuid) {

    UploadDto.uploadDto dto = uploadSessionCoreService.findByUuid(uuid);

    try {
      this.mergeChunks(
          dto.getTempPath(), dto.getFinalPath(), dto.getFileName(), dto.getChunkTotalIndex());
    } catch (IOException e) {
      return new DmlReturn("mergingfail", "chunk파일 merge 애러");
    }

    return new DmlReturn("success", "병합(merge) 정상처리되었습니다.");
  }

  public boolean checkChunkFoler(UploadDto.UploadRes upRes, String tmpDataSetDir, String fianlDir) {
    if (!FIleChecker.mkDir(tmpDataSetDir)) {
      upRes.setRes("fail");
      upRes.setRes("CHUNK 폴더 생성 ERROR");
      return false;
    }

    if (!FIleChecker.mkDir(fianlDir)) {
      upRes.setRes("fail");
      upRes.setRes("업로드 완료 폴더 생성 ERROR");
      return false;
    }

    return true;
  }

  public UploadDto.uploadDto checkUploadSession(
      UploadDto.UploadAddReq upAddReqDto, UploadDto.UploadRes upRes) {

    upRes.setRes("success");
    upRes.setResMsg("정상처리되었습니다.");

    UploadDto.uploadDto dto =
        uploadSessionCoreService.findByDatasetUid(
            upAddReqDto.getUploadDivi(), upAddReqDto.getUuid());

    if (upAddReqDto.getChunkIndex() == 0) {
      if (dto != null) {
        upRes.setRes("duplicate");
        upRes.setResMsg("이미 진행중인 업로드세션입니다.");
        return dto;
      }

      upAddReqDto.setStatus("UPLOADING");
      upRes.setUuid(upAddReqDto.getUuid().toString());
      uploadSessionCoreService.createUploadSession(upAddReqDto);
    } else {
      if (dto == null) {
        upRes.setRes("nosession");
        upRes.setResMsg("업로드 세션이 존재하지 않습니다.");
        return dto;
      }

      upAddReqDto.setStatus("UPLOADING");
      upAddReqDto.setUploadId(dto.getUploadId());
      uploadSessionCoreService.updateUploadSessionStatus(upAddReqDto);
    }

    if (dto != null) upRes.setUuid(dto.getUuid().toString());

    upRes.setChunkIndex(upAddReqDto.getChunkIndex());
    upRes.setChunkTotalIndex(upAddReqDto.getChunkTotalIndex());

    return dto;
  }

  public void mergeChunks(String tmpDir, String finalDir, String fileName, int chunkTotalIndex)
      throws IOException {
    long start = System.currentTimeMillis();
    Path outputPath = Paths.get(finalDir, fileName);

    log.info(
        "mergeChunks start: fileName={}, tmpDir={}, outputPath={}, lastChunkIndex={}",
        fileName,
        tmpDir,
        outputPath,
        chunkTotalIndex);

    long totalBytes = 0;

    try (FileChannel outChannel =
        FileChannel.open(
            outputPath,
            StandardOpenOption.CREATE,
            StandardOpenOption.WRITE,
            StandardOpenOption.TRUNCATE_EXISTING)) {

      for (int i = 0; i <= chunkTotalIndex; i++) {
        Path chunkPath = Paths.get(tmpDir, String.valueOf(i));

        try (FileChannel inChannel = FileChannel.open(chunkPath, StandardOpenOption.READ)) {
          long size = inChannel.size();
          long transferred = 0;
          while (transferred < size) {
            transferred += inChannel.transferTo(transferred, size - transferred, outChannel);
          }
          totalBytes += size;
        }

        Files.delete(chunkPath);
      }

      try {
        FIleChecker.deleteFolder(tmpDir);
        // 108 에서 86 서버로 이동
        //        log.info("################# server move 108 -> 86");
        //        FIleChecker.uploadTo86(outputPath);
      } catch (Exception e) {
        log.warn("tmpDir delete failed (merge already succeeded): tmpDir={}", tmpDir, e);
      }

    } catch (IOException e) {
      log.error(
          "mergeChunks failed: fileName={}, tmpDir={}, outputPath={}, lastChunkIndex={}",
          fileName,
          tmpDir,
          outputPath,
          chunkTotalIndex,
          e);
      throw e;
    }

    log.info(
        "mergeChunks done: fileName={}, outputPath={}, bytes={}, elapsedMs={}",
        fileName,
        outputPath,
        totalBytes,
        (System.currentTimeMillis() - start));
  }
}
