package com.eshore.gringotts.web.domain.controller;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.eshore.gringotts.web.configuration.HostConfiguration;
import com.eshore.gringotts.web.configuration.UploadConfiguration;
import com.eshore.gringotts.web.configuration.amis.AmisResponse;
import com.eshore.gringotts.web.domain.entity.DataFile;
import com.eshore.gringotts.web.domain.service.DataFileService;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import javax.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.collections.api.list.ImmutableList;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 * 文件上传接口
 *
 * @author lanyuanxiaoyao
 * @date 2024-11-21
 */
@Slf4j
@RestController
@RequestMapping("/upload")
public class DataFileController {
    private final HostConfiguration hostConfiguration;
    private final DataFileService dataFileService;
    private final String uploadFolderPath;
    private final String cacheFolderPath;
    private final String sliceFolderPath;

    public DataFileController(HostConfiguration hostConfiguration, UploadConfiguration uploadConfiguration, DataFileService dataFileService) {
        this.hostConfiguration = hostConfiguration;
        this.dataFileService = dataFileService;

        this.uploadFolderPath = uploadConfiguration.getUploadPath();
        this.cacheFolderPath = StrUtil.format("{}/cache", uploadFolderPath);
        this.sliceFolderPath = StrUtil.format("{}/slice", uploadFolderPath);
    }

    @PostMapping("")
    public AmisResponse<FinishResponse> upload(@RequestParam("file") MultipartFile file) throws IOException {
        String filename = file.getOriginalFilename();
        Long id = dataFileService.initialDataFile(filename);
        String url = StrUtil.format("{}/upload/download/{}", hostConfiguration.getPrefix(), id);
        byte[] bytes = file.getBytes();
        String originMd5 = SecureUtil.md5(new ByteArrayInputStream(bytes));
        File targetFile = new File(StrUtil.format("{}/{}", uploadFolderPath, originMd5));
        if (targetFile.exists()) {
            dataFileService.updateDataFile(id, FileUtil.getAbsolutePath(targetFile), FileUtil.size(targetFile), originMd5, file.getContentType());
            return AmisResponse.responseSuccess(new FinishResponse(id, filename, url, url));
        }
        File cacheFile = new File(StrUtil.format("{}/{}", cacheFolderPath, id));
        cacheFile = FileUtil.writeBytes(bytes, cacheFile);
        String targetMd5 = SecureUtil.md5(cacheFile);
        if (!StrUtil.equals(originMd5, targetMd5)) {
            throw new RuntimeException("文件上传失败，校验不匹配");
        }
        FileUtil.move(cacheFile, targetFile, true);
        dataFileService.updateDataFile(id, FileUtil.getAbsolutePath(targetFile), FileUtil.size(targetFile), targetMd5, file.getContentType());
        return AmisResponse.responseSuccess(new FinishResponse(id, filename, url, url));
    }

    @GetMapping("/download/{id}")
    public void download(@PathVariable Long id, HttpServletResponse response) throws IOException {
        DataFile dataFile = dataFileService.downloadFile(id);
        File targetFile = new File(dataFile.getPath());
        response.setHeader("Access-Control-Expose-Headers", "Content-Type");
        response.setHeader("Content-Type", dataFile.getType());
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        response.setHeader("Content-Disposition", StrUtil.format("attachment; filename={}", dataFile.getFilename()));
        IoUtil.copy(new FileInputStream(targetFile), response.getOutputStream());
    }

    @PostMapping("/start")
    public AmisResponse<StartResponse> start(@RequestBody StartRequest request) {
        log.info("Request: {}", request);
        Long id = dataFileService.initialDataFile(request.filename);
        return AmisResponse.responseSuccess(new StartResponse(id.toString()));
    }

    @PostMapping("/slice")
    public AmisResponse<SliceResponse> slice(
            @RequestParam("uploadId")
            Long uploadId,
            @RequestParam("partNumber")
            Integer sequence,
            @RequestParam("partSize")
            Long size,
            @RequestParam("file")
            MultipartFile file
    ) throws IOException {
        byte[] bytes = file.getBytes();
        String md5 = SecureUtil.md5(new ByteArrayInputStream(bytes));
        String targetFilename = StrUtil.format("{}-{}", sequence, md5);
        String targetFilePath = sliceFilePath(uploadId, targetFilename);
        FileUtil.mkParentDirs(targetFilePath);
        FileUtil.writeBytes(bytes, targetFilePath);
        return AmisResponse.responseSuccess(new SliceResponse(targetFilename));
    }

    private String sliceFilePath(Long uploadId, String sliceFilename) {
        return StrUtil.format("{}/{}/{}", sliceFolderPath, uploadId, sliceFilename);
    }

    @PostMapping("finish")
    public AmisResponse<FinishResponse> finish(@RequestBody FinishRequest request) {
        if (request.partList.anySatisfy(part -> !FileUtil.exist(sliceFilePath(request.uploadId, part.eTag)))) {
            throw new RuntimeException("文件校验失败，请重新上传");
        }
        try {
            File cacheFile = new File(StrUtil.format("{}/{}", cacheFolderPath, request.uploadId));
            FileUtil.mkParentDirs(cacheFile);
            if (cacheFile.createNewFile()) {
                try (FileOutputStream fos = new FileOutputStream(cacheFile)) {
                    try (FileChannel fosChannel = fos.getChannel()) {
                        for (FinishRequest.Part part : request.partList) {
                            File sliceFile = new File(sliceFilePath(request.uploadId, part.eTag));
                            try (FileInputStream fis = new FileInputStream(sliceFile)) {
                                try (FileChannel fisChannel = fis.getChannel()) {
                                    fisChannel.transferTo(0, fisChannel.size(), fosChannel);
                                }
                            }
                        }
                    }
                }
                String md5 = SecureUtil.md5(cacheFile);
                File targetFile = new File(StrUtil.format("{}/{}", uploadFolderPath, md5));
                if (!targetFile.exists()) {
                    FileUtil.move(cacheFile, targetFile, true);
                }
                String absolutePath = FileUtil.getAbsolutePath(targetFile);
                dataFileService.updateDataFile(
                        request.uploadId,
                        absolutePath,
                        FileUtil.size(targetFile),
                        SecureUtil.md5(targetFile),
                        FileUtil.getMimeType(absolutePath)
                );
                return AmisResponse.responseSuccess(new FinishResponse(
                        request.uploadId,
                        request.filename,
                        request.uploadId.toString(),
                        StrUtil.format("{}/upload/download/{}", hostConfiguration.getPrefix(), request.uploadId)
                ));
            } else {
                throw new RuntimeException("合并文件失败");
            }
        } catch (Throwable throwable) {
            throw new RuntimeException(throwable);
        } finally {
            FileUtil.del(StrUtil.format("{}/{}", cacheFolderPath, request.uploadId));
            FileUtil.del(StrUtil.format("{}/{}", sliceFolderPath, request.uploadId));
        }
    }

    @Data
    public static final class StartRequest {
        private String name;
        private String filename;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static final class StartResponse {
        private String uploadId;
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static final class SliceResponse {
        @JsonProperty("eTag")
        private String eTag;
    }

    @Data
    public static final class FinishRequest {
        private String filename;
        private Long uploadId;
        private ImmutableList<Part> partList;

        @Data
        public static final class Part {
            private Integer partNumber;
            @JsonProperty("eTag")
            private String eTag;
        }
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static final class FinishResponse {
        private Long id;
        private String filename;
        private String value;
        private String url;
    }
}
