package cn.reghao.tnb.file.app.service;

import cn.reghao.jutil.jdk.converter.ByteConverter;
import cn.reghao.jutil.jdk.converter.ByteType;
import cn.reghao.jutil.jdk.security.DigestUtil;
import cn.reghao.jutil.jdk.web.db.Page;
import cn.reghao.jutil.jdk.web.db.PageList;
import cn.reghao.tnb.common.auth.UserContext;
import cn.reghao.tnb.common.web.ServletUtil;
import cn.reghao.tnb.file.app.config.AppProperties;
import cn.reghao.tnb.file.app.db.mapper.LocalFileMapper;
import cn.reghao.tnb.file.app.model.po.LocalFile;
import cn.reghao.tnb.file.app.model.vo.FileQuery;
import cn.reghao.tnb.file.app.model.vo.LocalFileInfo;
import cn.reghao.tnb.file.app.model.vo.LocalFileUrl;
import cn.reghao.oss.api.constant.ObjectType;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * @author reghao
 * @date 2024-01-19 21:29:24
 */
@Slf4j
@Service
public class FileService {
    // 8MiB
    private final int bufSize = 1024*1024*8;
    private final ByteConverter byteConverter;
    private final LocalFileMapper localFileMapper;
    private final int pageSize = 12;
    private final String baseDir;

    public FileService(ByteConverter byteConverter, LocalFileMapper localFileMapper, AppProperties appProperties) {
        this.byteConverter = byteConverter;
        this.localFileMapper = localFileMapper;
        this.baseDir = appProperties.getBaseDir();
    }

    @PostConstruct
    public void createDir() {
        File dir = new File(this.baseDir);
        if (!dir.exists()) {
            try {
                FileUtils.forceMkdirParent(dir);
            } catch (IOException e) {
                log.error("create dir {} failed: {}", this.baseDir, e.getMessage());
            }
        }
    }

    public void getFile(String objectName) throws IOException {
        LocalFile diskFile = getByObjectName(objectName);
        if (diskFile == null) {
            log.error("LocalFile {} not exist", objectName);
            writeResponse(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String absolutePath = diskFile.getAbsolutePath();
        File file = new File(absolutePath);
        if (!file.exists() || file.isDirectory()) {
            log.error("file {} not exist", absolutePath);
            writeResponse(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String contentType = diskFile.getContentType();
        long size = diskFile.getSize();
        HttpServletResponse response = ServletUtil.getResponse();
        response.setStatus(HttpServletResponse.SC_OK);
        response.setContentType(contentType);
        response.setContentLengthLong(size);

        OutputStream outputStream = response.getOutputStream();
        writeResponse(outputStream, absolutePath, 0, size);
    }

    @Cacheable(cacheNames = "filePaths", key = "#objectName", unless = "#result == null")
    public LocalFile getByObjectName(String objectName) {
        FileQuery fileQuery = new FileQuery.Builder()
                .objectName(objectName)
                .build();
        Page page = new Page(1, pageSize);
        List<LocalFile> localFiles = localFileMapper.findFileQueryByPage(page, fileQuery);
        return localFiles.isEmpty() ? null : localFiles.get(0);
    }

    private void writeResponse(int statusCode) throws IOException {
        HttpServletResponse response = ServletUtil.getResponse();
        response.setStatus(statusCode);
        OutputStream outputStream = response.getOutputStream();
        outputStream.flush();
        outputStream.close();
    }

    private void writeResponse(OutputStream outputStream, String absolutePath, long start, long end) throws IOException {
        RandomAccessFile raf = new RandomAccessFile(absolutePath, "r");
        raf.seek(start);

        long len = end-start+1;
        if (len < bufSize) {
            int len1 = (int) len;
            byte[] buf1 = new byte[len1];
            int readLen1 = raf.read(buf1, 0, len1);
            outputStream.write(buf1, 0, readLen1);
        } else {
            byte[] buf = new byte[bufSize];
            long totalRead = 0;
            int readLen;
            while ((readLen = raf.read(buf, 0, bufSize)) != -1) {
                outputStream.write(buf, 0, readLen);
                totalRead += readLen;

                long left = len - totalRead;
                if (left < bufSize) {
                    int left1 = (int) left;
                    byte[] buf1 = new byte[left1];
                    int readLen1 = raf.read(buf1, 0, left1);
                    outputStream.write(buf1, 0, readLen1);
                    break;
                }
            }
        }

        outputStream.flush();
        outputStream.close();
        raf.close();
    }

    public LocalFile putLocalFile(MultipartFile file) throws Exception {
        long size = file.getSize();
        String filename = file.getOriginalFilename();
        String suffix = getSuffix(filename);

        String objectId = UUID.randomUUID().toString().replace("-", "");;
        String objectName;
        if (suffix.isBlank()) {
            objectName = String.format("file/%s", objectId);
        } else {
            objectName = String.format("file/%s%s", objectId, suffix);
        }

        String contentId = UUID.randomUUID().toString().replace("-", "");
        String destPath = String.format("%s/%s", baseDir, contentId);
        File savedFile = saveFile(file.getInputStream(), destPath);
        String contentType = "image/jpeg";
        int fileType = ObjectType.Image.getValue();
        String sha256sum = DigestUtil.sha256sum(savedFile.getAbsolutePath());
        FileQuery fileQuery = new FileQuery.Builder()
                .sha256sum(sha256sum)
                .build();

        Page page = new Page(1, pageSize);
        List<LocalFile> diskFiles = localFileMapper.findFileQueryByPage(page, fileQuery);
        LocalFile diskFile;
        if (!diskFiles.isEmpty()) {
            LocalFile existFile = diskFiles.get(0);
            diskFile = new LocalFile(objectName, objectId, existFile, filename);
            FileUtils.deleteQuietly(savedFile);
        } else {
            diskFile = new LocalFile(objectName, objectId, savedFile.getAbsolutePath(), sha256sum, filename, contentType, fileType, size);
        }

        localFileMapper.save(diskFile);
        return diskFile;
    }

    private File saveFile(InputStream inputStream, String absolutePath) throws IOException {
        File file = new File(absolutePath);
        if (file.exists()) {
            throw new IOException(absolutePath + " exist");
        }

        Files.copy(inputStream, Path.of(absolutePath), StandardCopyOption.REPLACE_EXISTING);
        return file;
    }

    private String getSuffix(String filename) {
        if (filename == null) {
            return "";
        }

        int idx = filename.lastIndexOf(".");
        return idx == -1 ? "" : filename.substring(idx);
    }

    public PageList<LocalFileInfo> getLocalFiles(int pageNumber) {
        long owner = UserContext.getUserId();
        FileQuery fileQuery = new FileQuery.Builder()
                .owner(owner)
                .build();

        Page page = new Page(pageNumber, pageSize);
        int total = localFileMapper.countByFileQuery(fileQuery);
        List<LocalFile> list = localFileMapper.findFileQueryByPage(page, fileQuery);

        List<LocalFileInfo> list0 = list.stream().map(diskFile -> {
            String size = byteConverter.convert(ByteType.Bytes, diskFile.getSize());
            return new LocalFileInfo(diskFile, size);
        }).collect(Collectors.toList());
        return PageList.empty();
    }

    public PageList<LocalFileUrl> getImageFiles(int pageNumber) {
        long owner = UserContext.getUserId();
        int fileType = ObjectType.Image.getValue();
        FileQuery fileQuery = new FileQuery.Builder()
                .fileType(fileType)
                .owner(owner)
                .build();
        Page page = new Page(pageNumber, pageSize);
        int total = localFileMapper.countByFileQuery(fileQuery);
        List<LocalFile> list = localFileMapper.findFileQueryByPage(page, fileQuery);
        List<LocalFileUrl> list0 = list.stream().map(diskFile -> {
            String fileId = diskFile.getObjectId();
            String filename = diskFile.getFilename();
            String objectName = diskFile.getObjectName();
            String url = String.format("/%s", objectName);
            return new LocalFileUrl(fileId, filename, url);
        }).collect(Collectors.toList());
        return PageList.pageList(pageNumber, pageSize, total, list0);
    }

    public LocalFile getLocalFile(String objectId) {
        FileQuery fileQuery = new FileQuery.Builder()
                .objectId(objectId)
                .build();

        Page page = new Page(1, 1);
        List<LocalFile> list = localFileMapper.findFileQueryByPage(page, fileQuery);
        return list.isEmpty() ? null : list.get(0);
    }

    public String getObjectName(String path) {
        String objectName = "";
        if (!path.equals("/")) {
            objectName = path.substring(1) + "/";
        }

        return objectName;
    }
}
