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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.kamco.cd.training.common.enums.LearnDataType;
import com.kamco.cd.training.common.exception.CustomApiException;
import com.kamco.cd.training.common.service.FormatStorage;
import com.kamco.cd.training.common.utils.FIleChecker;
import com.kamco.cd.training.config.api.ApiResponseDto.ApiResponseCode;
import com.kamco.cd.training.config.api.ApiResponseDto.ResponseObj;
import com.kamco.cd.training.dataset.dto.DatasetDto;
import com.kamco.cd.training.dataset.dto.DatasetDto.AddReq;
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetMngRegDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetClass;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetObjRegDto;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.DatasetStorage;
import com.kamco.cd.training.dataset.dto.DatasetObjDto.SearchReq;
import com.kamco.cd.training.postgres.core.DatasetCoreService;
import jakarta.validation.Valid;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class DatasetService {

  private final DatasetCoreService datasetCoreService;

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

  private static final List<String> LABEL_DIRS = List.of("label-json", "label", "input1", "input2");

  /**
   * 데이터셋 목록 조회
   *
   * @param searchReq 검색 조건
   * @return 데이터셋 목록
   */
  public Page<DatasetDto.Basic> searchDatasets(DatasetDto.SearchReq searchReq) {
    log.info("데이터셋 목록 조회 - 조건: {}", searchReq);
    return datasetCoreService.findDatasetList(searchReq);
  }

  /**
   * 데이터셋 상세 조회
   *
   * @param id 상세 조회할 목록 Id
   * @return 데이터셋 상세 정보
   */
  public DatasetDto.Basic getDatasetDetail(UUID id) {
    return datasetCoreService.getOneByUuid(id);
  }

  /**
   * 데이터셋 등록
   *
   * @param registerReq 등록 요청
   * @return 등록된 데이터셋 ID
   */
  @Transactional
  public Long registerDataset(DatasetDto.RegisterReq registerReq) {
    log.info("데이터셋 등록 - 요청: {}", registerReq);
    DatasetDto.Basic saved = datasetCoreService.save(registerReq);
    log.info("데이터셋 등록 완료 - ID: {}", saved.getId());
    return saved.getId();
  }

  /**
   * 데이터셋 수정
   *
   * @param updateReq 수정 요청
   * @return 수정된 데이터셋 ID
   */
  @Transactional
  public void updateDataset(UUID uuid, DatasetDto.UpdateReq updateReq) {
    datasetCoreService.update(uuid, updateReq);
  }

  /**
   * 데이터셋 삭제 (다건)
   *
   * @param uuid 삭제 요청
   */
  @Transactional
  public void deleteDatasets(UUID uuid) {
    datasetCoreService.deleteDatasets(uuid);
  }

  /**
   * 데이터셋 통계 요약
   *
   * @param summaryReq 요약 요청
   * @return 통계 요약
   */
  public DatasetDto.Summary getDatasetSummary(DatasetDto.SummaryReq summaryReq) {
    log.info("데이터셋 통계 요약 - 요청: {}", summaryReq);
    return datasetCoreService.getDatasetSummary(summaryReq);
  }

  public Page<DatasetObjDto.Basic> searchDatasetObjectList(SearchReq searchReq) {
    return datasetCoreService.searchDatasetObjectList(searchReq);
  }

  public UUID deleteDatasetObjByUuid(UUID uuid) {
    return datasetCoreService.deleteDatasetObjByUuid(uuid);
  }

  /**
   * 데이터셋 object class 조회
   *
   * @param uuid dataset uuid
   * @param type compare, target
   */
  public List<DatasetClass> getDatasetObjByUuid(UUID uuid, String type) {
    return datasetCoreService.findDatasetObjClassByUuid(uuid, type);
  }

  /**
   * 사용 가능 공간 조회
   *
   * @return
   */
  public DatasetStorage getUsableBytes() {
    // 현재 실행 위치가 속한 디스크 기준
    try {
      FormatStorage.DiskUsage usage = FormatStorage.getDiskUsage(Path.of("."));
      log.debug("경로 : {}", usage.path());
      log.debug("총 저장공간 : {}", usage.totalText());
      log.debug("남은 저장공간 : {}", usage.usableText());
      log.debug("사용률 : {}", usage.usedPercent());

      DatasetStorage datasetStorage = new DatasetStorage();
      datasetStorage.setUsableBytes(usage.usableText());
      return datasetStorage;
    } catch (Exception e) {
      throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND);
    }
  }

  @Deprecated
  @Transactional
  public ResponseObj insertDatasetTo86(@Valid AddReq addReq) {

    Long datasetUid = null; // master id 값, 등록하면서 가져올 예정

    // 압축 해제
    FIleChecker.unzipOn86Server(
        addReq.getFilePath() + addReq.getFileName(),
        addReq.getFilePath() + addReq.getFileName().replace(".zip", ""));

    // 해제한 폴더 읽어서 데이터 저장
    List<Map<String, Object>> list =
        getUnzipDatasetFilesTo86(
            addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "train");

    int idx = 0;
    for (Map<String, Object> map : list) {
      datasetUid =
          this.insertTrainTestData(map, addReq, idx, datasetUid, "train"); // train 데이터 insert
      idx++;
    }

    List<Map<String, Object>> testList =
        getUnzipDatasetFilesTo86(
            addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "test");

    int testIdx = 0;
    for (Map<String, Object> test : testList) {
      datasetUid =
          this.insertTrainTestData(test, addReq, testIdx, datasetUid, "test"); // test 데이터 insert
      testIdx++;
    }

    datasetCoreService.updateDatasetUploadStatus(datasetUid);
    return new ResponseObj(ApiResponseCode.OK, "업로드 성공하였습니다.");
  }

  @Transactional
  public ResponseObj insertDataset(@Valid AddReq addReq) {

    Long datasetUid = null; // master id 값, 등록하면서 가져올 예정

    try {
      // 압축 해제
      FIleChecker.unzip(addReq.getFileName(), addReq.getFilePath());

      // 해제한 폴더 읽어서 데이터 저장
      List<Map<String, Object>> list =
          getUnzipDatasetFiles(
              addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "train");

      int idx = 0;
      for (Map<String, Object> map : list) {
        datasetUid =
            this.insertTrainTestData(map, addReq, idx, datasetUid, "train"); // train 데이터 insert
        idx++;
      }

      List<Map<String, Object>> valList =
          getUnzipDatasetFiles(
              addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "val");

      int valIdx = 0;
      for (Map<String, Object> valid : valList) {
        datasetUid =
            this.insertTrainTestData(valid, addReq, valIdx, datasetUid, "val"); // val 데이터 insert
        valIdx++;
      }

      List<Map<String, Object>> testList =
          getUnzipDatasetFiles(
              addReq.getFilePath() + addReq.getFileName().replace(".zip", ""), "test");

      int testIdx = 0;
      for (Map<String, Object> test : testList) {
        datasetUid =
            this.insertTrainTestData(test, addReq, testIdx, datasetUid, "test"); // test 데이터 insert
        testIdx++;
      }

    } catch (IOException e) {
      log.error(e.getMessage());
      return new ResponseObj(ApiResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
    }

    datasetCoreService.updateDatasetUploadStatus(datasetUid);
    return new ResponseObj(ApiResponseCode.OK, "업로드 성공하였습니다.");
  }

  @Transactional
  public Long insertTrainTestData(
      Map<String, Object> map, AddReq addReq, int idx, Long datasetUid, String subDir) {
    ObjectMapper mapper = new ObjectMapper();
    String comparePath = (String) map.get("input1");
    String targetPath = (String) map.get("input2");
    String labelPath = (String) map.get("label");
    String geojsonPath = (String) map.get("geojson_path");
    Object labelJson = map.get("label-json");
    JsonNode json;

    if (labelJson instanceof JsonNode jn) {
      json = jn;
    } else {
      try {
        json = mapper.readTree(labelJson.toString());
      } catch (IOException e) {
        throw new RuntimeException("label_json parse error", e);
      }
    }

    String fileName = Paths.get(comparePath).getFileName().toString();
    String[] fileNameStr = fileName.split("_");
    String compareYyyy = fileNameStr[1];
    String targetYyyy = fileNameStr[2];
    String mapSheetNum = fileNameStr[3];

    if (idx == 0 && subDir.equals("train")) {
      String title = compareYyyy + "-" + targetYyyy; // 제목 : 비교년도-기준년도
      String dataType = LearnDataType.PRODUCTION.getId(); // 만들어 넣는 건 다 제작
      Long stage =
          datasetCoreService.getDatasetMaxStage(
                  Integer.parseInt(compareYyyy), Integer.parseInt(targetYyyy))
              + 1;
      String uid = addReq.getFileName().replace(".zip", "");

      DatasetMngRegDto mngRegDto =
          DatasetMngRegDto.builder()
              .uid(uid)
              .dataType(dataType)
              .compareYyyy(Integer.parseInt(compareYyyy))
              .targetYyyy(Integer.parseInt(targetYyyy))
              .title(title)
              .memo(addReq.getMemo())
              .roundNo(stage)
              .totalSize(addReq.getFileSize())
              .datasetPath(addReq.getFilePath())
              .build();

      datasetUid = datasetCoreService.insertDatasetMngData(mngRegDto); // tb_dataset 에 insert
    }

    // datasetUid 로 obj 도 등록하기
    // Json 갯수만큼 for문 돌려서 insert 해야 함, features에 빈값
    if (json != null && json.path("features") != null && !json.path("features").isEmpty()) {

      for (JsonNode feature : json.path("features")) {
        JsonNode prop = feature.path("properties");
        String compareClassCd = prop.path("before").asText(null);
        String targetClassCd = prop.path("after").asText(null);

        // 한 개씩 자른 geojson을 FeatureCollection 으로 만들어서 넣기
        ObjectNode root = mapper.createObjectNode();
        root.put("type", "FeatureCollection");
        ArrayNode features = mapper.createArrayNode();
        features.add(feature);
        root.set("features", features);

        DatasetObjRegDto objRegDto =
            DatasetObjRegDto.builder()
                .datasetUid(datasetUid)
                .compareYyyy(Integer.parseInt(compareYyyy))
                .compareClassCd(compareClassCd)
                .targetYyyy(Integer.parseInt(targetYyyy))
                .targetClassCd(targetClassCd)
                .comparePath(comparePath)
                .targetPath(targetPath)
                .labelPath(labelPath)
                .mapSheetNum(mapSheetNum)
                .geojson(root)
                .geojsonPath(geojsonPath)
                .fileName(fileName)
                .build();

        if (subDir.equals("train")) {
          datasetCoreService.insertDatasetObj(objRegDto);
        } else if (subDir.equals("val")) {
          datasetCoreService.insertDatasetValObj(objRegDto);
        } else {
          datasetCoreService.insertDatasetTestObj(objRegDto);
        }
      }
    }

    return datasetUid;
  }

  private List<Map<String, Object>> getUnzipDatasetFiles(String unzipRootPath, String subDir) {

    Path root = Paths.get(unzipRootPath).resolve(subDir);
    Map<String, Map<String, Object>> grouped = new HashMap<>();

    for (String dirName : LABEL_DIRS) {
      Path dir = root.resolve(dirName);

      if (!Files.isDirectory(dir)) {
        throw new IllegalStateException("폴더가 존재하지 않습니다 : " + dir);
      }

      try (Stream<Path> stream = Files.list(dir)) {
        stream
            .filter(Files::isRegularFile)
            .forEach(
                path -> {
                  String fileName = path.getFileName().toString();
                  String baseName = getBaseName(fileName);

                  // baseName 기준 Map 생성
                  Map<String, Object> data =
                      grouped.computeIfAbsent(baseName, k -> new HashMap<>());

                  // 공통 메타
                  data.put("baseName", baseName);

                  // 폴더별 처리
                  if ("label-json".equals(dirName)) {
                    // json 파일이면 파싱
                    data.put("label-json", readJson(path));
                    data.put("geojson_path", path.toAbsolutePath().toString());
                  } else {
                    data.put(dirName, path.toAbsolutePath().toString());
                  }
                });

      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    return new ArrayList<>(grouped.values());
  }

  private String getBaseName(String fileName) {
    int idx = fileName.lastIndexOf('.');
    return (idx > 0) ? fileName.substring(0, idx) : fileName;
  }

  private JsonNode readJson(Path jsonPath) {
    try {
      ObjectMapper mapper = new ObjectMapper();
      return mapper.readTree(jsonPath.toFile());
    } catch (IOException e) {
      throw new RuntimeException("JSON 읽기 실패: " + jsonPath, e);
    }
  }

  public String getFilePathByUUIDPathType(UUID uuid, String pathType) {
    return datasetCoreService.getFilePathByUUIDPathType(uuid, pathType);
  }

  @Deprecated
  private List<Map<String, Object>> getUnzipDatasetFilesTo86(String unzipRootPath, String subDir) {

    //    String root = Paths.get(unzipRootPath)
    //      .resolve(subDir)
    //      .toString();
    //
    String root = normalizeLinuxPath(unzipRootPath + "/" + subDir);

    Map<String, Map<String, Object>> grouped = new HashMap<>();

    for (String dirName : LABEL_DIRS) {

      String remoteDir = root + "/" + dirName;

      // 1. 86 서버에서 해당 디렉토리의 파일 목록 조회
      List<String> files = listFilesOn86Server(remoteDir);

      if (files.isEmpty()) {
        throw new IllegalStateException("폴더가 존재하지 않거나 파일이 없습니다 : " + remoteDir);
      }

      for (String fullPath : files) {

        String fileName = Paths.get(fullPath).getFileName().toString();
        String baseName = getBaseName(fileName);

        Map<String, Object> data = grouped.computeIfAbsent(baseName, k -> new HashMap<>());

        data.put("baseName", baseName);

        if ("label-json".equals(dirName)) {

          // 2. json 내용도 86 서버에서 읽어서 가져와야 함
          String json = readRemoteFileAsString(fullPath);

          data.put("label-json", parseJson(json));
          data.put("geojson_path", fullPath);

        } else {

          data.put(dirName, fullPath);
        }
      }
    }

    return new ArrayList<>(grouped.values());
  }

  private List<String> listFilesOn86Server(String remoteDir) {

    String command = "find " + escape(remoteDir) + " -maxdepth 1 -type f";

    return FIleChecker.execCommandAndReadLines(command);
  }

  private String readRemoteFileAsString(String remoteFilePath) {

    String command = "cat " + escape(remoteFilePath);

    List<String> lines = FIleChecker.execCommandAndReadLines(command);

    return String.join("\n", lines);
  }

  private JsonNode parseJson(String json) {
    try {
      ObjectMapper mapper = new ObjectMapper();
      return mapper.readTree(json);
    } catch (IOException e) {
      throw new RuntimeException("JSON 파싱 실패", e);
    }
  }

  private String escape(String path) {
    return "'" + path.replace("'", "'\"'\"'") + "'";
  }

  private static String normalizeLinuxPath(String path) {
    return path.replace("\\", "/");
  }

  public ResponseEntity<Resource> getFilePathByFile(String remoteFilePath) {

    String host = "192.168.2.86";
    String user = "kcomu";
    String password = "Kamco2025!";

    Session session = null;
    ChannelSftp sftp = null;

    try {
      JSch jsch = new JSch();

      session = jsch.getSession(user, host, 22);
      session.setPassword(password);

      Properties config = new Properties();
      config.put("StrictHostKeyChecking", "no");
      session.setConfig(config);
      session.connect(10_000);

      sftp = (ChannelSftp) session.openChannel("sftp");
      sftp.connect(5_000);

      // 86 서버 파일을 스트림으로 연다
      InputStream inputStream = sftp.get(normalizeLinuxPath(remoteFilePath));

      InputStreamResource resource =
          new InputStreamResource(inputStream) {
            @Override
            public long contentLength() {
              return -1; // 알 수 없으면 -1
            }
          };

      String fileName = Paths.get(remoteFilePath.replace("\\", "/")).getFileName().toString();

      return ResponseEntity.ok()
          .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
          .contentType(MediaType.APPLICATION_OCTET_STREAM)
          .body(resource);

    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}
