package com.backendsys.modules.upload.service.impl;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.backendsys.exception.CustException;
import com.backendsys.modules.common.config.security.utils.HttpRequestUtil;
import com.backendsys.modules.common.config.security.utils.SecurityUtil;
import com.backendsys.modules.sdk.douyincloud.tos.service.DouyinTosService;
import com.backendsys.modules.sdk.tencentcloud.cos.service.TencentCosService;
import com.backendsys.modules.system.dao.SysUserDao;
import com.backendsys.modules.system.entity.SysCommon;
import com.backendsys.modules.system.entity.SysUser;
import com.backendsys.modules.system.service.SysCommonService;
import com.backendsys.modules.upload.dao.SysFileDao;
import com.backendsys.modules.upload.entity.MultipartUploadParams;
import com.backendsys.modules.upload.entity.SysFile;
import com.backendsys.modules.upload.enums.StyleEnums;
import com.backendsys.modules.upload.enums.TargetEnums;
import com.backendsys.modules.upload.service.SysFileMultipartService;
import com.backendsys.utils.response.ResultEnum;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qcloud.cos.model.*;
import com.volcengine.tos.model.object.CompleteMultipartUploadV2Output;
import com.volcengine.tos.model.object.CreateMultipartUploadOutput;
import com.volcengine.tos.model.object.UploadPartV2Output;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

@Service
public class SysFileMultipartServiceImpl implements SysFileMultipartService {

    @Value("${tencent.cos.accessible-domain}")
    private String TENCENT_ACCESSIBLE_DOMAIN;

    @Value("${douyin.tos.domain}")
    private String DOUYIN_ACCESSIBLE_DOMAIN;

    @Lazy
    @Autowired
    RedissonClient redissonClient;
    @Autowired
    private HttpRequestUtil httpRequestUtil;
    @Autowired
    private TencentCosService tencentCosService;
    @Autowired
    private DouyinTosService douyinTosService;
    @Autowired
    private SysCommonService sysCommonService;

    @Autowired
    private SysUserDao sysUserDao;
    @Autowired
    private SysFileDao sysFileDao;


    // [方法] 设置缩略图 (参数)
    // 不同的云环境 (target)，缩略图配置也不一样
    //   -1:本地:
    //   1:腾讯云: https://cloud.tencent.com/document/product/436/113295
    //   2:阿里云:
    //   3:抖音云: https://www.volcengine.com/docs/6349/153626
    private SysFile setThumbUrl(SysFile sysFile, Integer width, Integer height, String backgroundColor) {
        if (sysFile.getContent_type() != null) {
            if (sysFile.getContent_type().toLowerCase().contains("image")) {
                // 本地上传
                if (sysFile.getTarget() == -1) {
                    sysFile.setUrl_thumb(sysFile.getUrl() + "?w=" + width + "&h=" + height);
                }
                // 腾讯云 (color值通过base64加密, #f8f8f8)
                if (sysFile.getTarget() == 1) {
                    backgroundColor = "#" + backgroundColor;
                    System.out.println("base64 encode: " + Base64.encode(backgroundColor));
                    sysFile.setUrl_thumb(sysFile.getUrl() + "?imageMogr2/thumbnail/" + width + "x" + height + "/pad/1/color/" + Base64.encode(backgroundColor));
                }
                // 抖音云
                if (sysFile.getTarget() == 3) {
                    sysFile.setUrl_thumb(sysFile.getUrl() + "?x-tos-process=image/resize,w_" + width + ",h_" + height + ",m_pad,color_" + backgroundColor);
                }
            }
        }
        return sysFile;
    }

    // [方法] 上传事件
    private SysFile uploadInitEvent(MultipartUploadParams multipartUploadParams, Integer target) {
        System.out.println("[系统配置] 上传目标(-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云): " + target);

        String upload_id = null;
        String object_key = null;
        String object_name = Convert.toStr(UUID.randomUUID()) + "." + FileUtil.extName(multipartUploadParams.getFilename());

        // target: 上传目标 (-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云)
        if (target == 1) {
            InitiateMultipartUploadResult uploadResult = tencentCosService.initiateMultipartUpload(object_name);
            upload_id = uploadResult.getUploadId();
            object_key = uploadResult.getKey();
        }
        // 3: 抖音云
        if (target == 3) {
            CreateMultipartUploadOutput uploadResult = douyinTosService.initiateMultipartUpload(object_name);
            upload_id = uploadResult.getUploadID();
            object_key = uploadResult.getKey();
        }

        // 如果没有获得以上参数，则抛出报错
        if (StrUtil.isEmpty(upload_id) || StrUtil.isEmpty(object_key)) {
            throw new CustException("upload_id 或 object_key 不能为空");
        }

        // [新增] 上传文件记录
        SysFile sysFileEntity = new SysFile();
        sysFileEntity.setCategory_id(multipartUploadParams.getCategory_id());
        sysFileEntity.setUser_id(httpRequestUtil.getUserId());
        sysFileEntity.setUpload_id(upload_id);
        sysFileEntity.setUpload_chunk_count(multipartUploadParams.getUpload_chunk_count());
        sysFileEntity.setUpload_chunk_index(0);
//        sysFileEntity.setName(FileNameUtil.getName(object_key));
        sysFileEntity.setName(multipartUploadParams.getFilename());
        sysFileEntity.setObject_key(object_key);
        sysFileEntity.setTarget(target);
        sysFileEntity.setContent_type(multipartUploadParams.getContent_type());
        sysFileEntity.setSize(multipartUploadParams.getSize());
        sysFileEntity.setMd5(multipartUploadParams.getMd5());
        sysFileDao.insert(sysFileEntity);

        return sysFileEntity;
    }


    // 1.初始化分块上传 (获得 upload_id, object_key)
    @Override
    public Map<String, Object> multipartUploadInit(MultipartUploadParams multipartUploadParams) {

//        // 按用户加锁
//        RLock lock = redissonClient.getLock("lock::multipart-upload::user-id::" + SecurityUtil.getUserId());
//        try { lock.tryLock(3, TimeUnit.SECONDS);

        // 获得公共配置
        List<SysCommon> sysCommonList = sysCommonService.getCommonByCategory("UPLOAD");
        AtomicReference<Integer> UPLOAD_TARGET = new AtomicReference<>();
        AtomicReference<Integer> UPLOAD_THUMB_SIZE = new AtomicReference<>();
        AtomicReference<Boolean> UPLOAD_MD5_DUPLICATE = new AtomicReference<>();
        sysCommonList.stream().forEach(sysCommon -> {
            if (sysCommon.getTag().equals("UPLOAD_TARGET")) UPLOAD_TARGET.set(Convert.toInt(sysCommon.getValue()));
            if (sysCommon.getTag().equals("UPLOAD_THUMB_SIZE")) UPLOAD_THUMB_SIZE.set(Convert.toInt(sysCommon.getValue()));
            if (sysCommon.getTag().equals("UPLOAD_MD5_DUPLICATE")) UPLOAD_MD5_DUPLICATE.set(Convert.toBool(sysCommon.getValue()));
        });

        SysFile sysFileEntity = null;

        // [弃用] 获取大文件流需要比较久的时间
        // String md5 = DigestUtil.md5Hex(multipartFile.getInputStream());

        // 如果开启了 MD5秒传，则判断文件是否上传过，是的话就直接返回
        // [系统配置] 是否启用文件MD5查重 (仅判断用户自己上传过的文件)
        // - 存在，则根据文件MD5 判断是否上传过文件 (仅更新时间，不上传已存在的文件，允许更新文件分类)
        // - 存在，如果存在两个相同 md5 以上的文件，则需要排重后再上传 (如果是不同用户
        // - 不存在，则走上传流程
        Boolean is_exist = false;
        if (UPLOAD_MD5_DUPLICATE.get()) {

            // 排除文件异常的情况（出现两个以上相同MD5的文件）
            LambdaQueryWrapper<SysFile> wrapperFile = new LambdaQueryWrapper<>();
            wrapperFile.eq(SysFile::getMd5, multipartUploadParams.getMd5());
            wrapperFile.eq(SysFile::getUser_id, SecurityUtil.getUserId());
            wrapperFile.isNull(SysFile::getUpload_id);

            // [DB] 查询已存在的文件分块记录 (只有完全上传成功，(有 md5，没有 upload_id 才算合并成功))
            // - 异常情况：如果有两个相同的文件，一个上传50%，一个上传成功，就会出现以下这种情况，需要手动解决
            List<SysFile> sysFileEntityList = sysFileDao.selectList(wrapperFile);
            if (sysFileEntityList != null && sysFileEntityList.size() > 1) {
                throw new CustException("存在 " + sysFileEntityList.size() + " 个相同MD5 (" + multipartUploadParams.getMd5() + ") 的文件", ResultEnum.STATUS_CONFLICT.getCode());
            }

            if (sysFileEntityList != null && sysFileEntityList.size() > 0) {
                is_exist = true;
                // 将已存在的文件，赋值
                sysFileEntity = sysFileEntityList.get(0);
                // [DB] 更新文件 (文件分类、上传时间)
                sysFileEntity.setCategory_id(multipartUploadParams.getCategory_id());

                LocalDateTime nowLocalDateTime = (new DateTime()).toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime();
                sysFileEntity.setUpload_time(nowLocalDateTime);

                sysFileDao.updateById(sysFileEntity);
            } else {
                // [DB] 创建新的文件
                sysFileEntity = uploadInitEvent(multipartUploadParams, UPLOAD_TARGET.get());
                // [格式化] 封面 (图片类型)
                sysFileEntity = setThumbUrl(sysFileEntity, UPLOAD_THUMB_SIZE.get(), UPLOAD_THUMB_SIZE.get(), StyleEnums.THUMB_BACKGROUND.getValue());
            }

        } else {
            // 不开启 MD5秒 传，则直接初始化分块
            // [DB] 创建新的文件
            sysFileEntity = uploadInitEvent(multipartUploadParams, UPLOAD_TARGET.get());
            // [格式化] 封面 (图片类型)
            sysFileEntity = setThumbUrl(sysFileEntity, UPLOAD_THUMB_SIZE.get(), UPLOAD_THUMB_SIZE.get(), StyleEnums.THUMB_BACKGROUND.getValue());
        }

        Map<String, Object> resp = new LinkedHashMap<>();
        resp.put("target", UPLOAD_TARGET.get());
        resp.put("target_label", TargetEnums.targetToLabel(UPLOAD_TARGET.get()));
        resp.put("upload_chunk_count", multipartUploadParams.getUpload_chunk_count());
        resp.put("upload_id", sysFileEntity.getUpload_id());
        resp.put("object_key", sysFileEntity.getObject_key());
        resp.put("md5", multipartUploadParams.getMd5());
        resp.put("is_exist", is_exist);
        resp.put("file_info", is_exist ? sysFileEntity : null);
        return resp;

//        } catch (InterruptedException e) { throw new RuntimeException(e);
//        } finally { lock.unlock(); }
    }

    // 2.上传分块
    // - 单个分块大小不能小于4MB
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> multipartUpload(MultipartFile multipartFile, String upload_id, Integer upload_chunk_index) {

        if (multipartFile.isEmpty()) throw new CustException("file 上传文件不能为空");
        if (StrUtil.isEmpty(upload_id)) throw new CustException("upload_id 不能为空");
        if (upload_chunk_index == null) throw new CustException("upload_chunk_index 不能为空");

//        // 按用户加锁
//        RLock lock = redissonClient.getLock("lock::multipart-upload::user-id::" + SecurityUtil.getUserId());
//        try { lock.tryLock(3, TimeUnit.SECONDS);

            // [Get] 获得初始化分块信息
            SysFile sysFileEntity = sysFileDao.selectOne(new LambdaQueryWrapper<SysFile>().eq(SysFile::getUpload_id, upload_id));
            if (sysFileEntity == null) throw new CustException("分块记录不存在");
            String object_key = sysFileEntity.getObject_key();
            Integer upload_chunk_size = sysFileEntity.getUpload_chunk_count();
            if (upload_chunk_index > upload_chunk_size) throw new CustException("分块索引(index)不能大于分块数量(count)");

            // 进入分块上传流程
            if (upload_chunk_index < upload_chunk_size) {
                // 分片编号从 1 开始，最大为 10000。除最后一个分片以外，其他分片大小最小为 4MiB
                if (multipartFile.getSize() < 4 * 1024 * 1024) throw new CustException("分块文件大小不能小于 4MB");
            }

            // 获得公共配置
            List<SysCommon> sysCommonList = sysCommonService.getCommonByCategory("UPLOAD");
            AtomicReference<Integer> UPLOAD_TARGET = new AtomicReference<>();
            sysCommonList.stream().forEach(sysCommon -> {
                if (sysCommon.getTag().equals("UPLOAD_TARGET")) UPLOAD_TARGET.set(Convert.toInt(sysCommon.getValue()));
            });
            System.out.println("[系统配置] 上传目标(-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云): " + UPLOAD_TARGET);


            String part_etag = null;

            // target: 上传目标 (-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云)
            if (UPLOAD_TARGET.get() == 1) {
                UploadPartResult partResult = tencentCosService.uploadPart(multipartFile, upload_id, object_key, upload_chunk_index);
                part_etag = partResult.getPartETag().getETag();
            }
            // 3: 抖音云
            if (UPLOAD_TARGET.get() == 3) {
                UploadPartV2Output partResult = douyinTosService.uploadPart(multipartFile, upload_id, object_key, upload_chunk_index);
                part_etag = partResult.getEtag().replace("\"", "");
            }

            // [Update] 更新分块文件信息
            sysFileEntity.setUpload_chunk_index(upload_chunk_index);
            sysFileDao.updateById(sysFileEntity);

            Map<String, Object> resp = new LinkedHashMap<>();
            resp.put("upload_chunk_index", upload_chunk_index);
            resp.put("upload_id", upload_id);
            resp.put("object_key", object_key);
            resp.put("part_etag", part_etag);
            return resp;

//        } catch (InterruptedException e) { throw new RuntimeException(e);
//        } finally { lock.unlock(); }
    }

    // 完成分块上传
    @Override
    public SysFile multipartUploadComplete(String upload_id, Integer is_watermark) {

        if (StrUtil.isEmpty(upload_id)) throw new CustException("upload_id 不能为空");

//        // 按用户加锁
//        RLock lock = redissonClient.getLock("lock::multipart-upload::user-id::" + SecurityUtil.getUserId());
//        try { lock.tryLock(3, TimeUnit.SECONDS);

            // [Get] 查询文件分块记录
            SysFile sysFileEntity = sysFileDao.selectOne(new LambdaQueryWrapper<SysFile>().eq(SysFile::getUpload_id, upload_id));
            if (sysFileEntity == null) throw new CustException("upload_id 不存在");

            // 判断分块索引和分块数量是否一致
            if (sysFileEntity.getUpload_chunk_index() != sysFileEntity.getUpload_chunk_count()) {
                throw new CustException("分块索引和分块数量不一致");
            }

            // 获得公共配置
            List<SysCommon> sysCommonList = sysCommonService.getCommonByCategory("UPLOAD");
            AtomicReference<Integer> UPLOAD_TARGET = new AtomicReference<>();
            AtomicReference<Integer> UPLOAD_THUMB_SIZE = new AtomicReference<>();
            sysCommonList.stream().forEach(sysCommon -> {
                if (sysCommon.getTag().equals("UPLOAD_TARGET")) UPLOAD_TARGET.set(Convert.toInt(sysCommon.getValue()));
                if (sysCommon.getTag().equals("UPLOAD_THUMB_SIZE")) UPLOAD_THUMB_SIZE.set(Convert.toInt(sysCommon.getValue()));
            });
            System.out.println("[系统配置] 上传目标(-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云): " + UPLOAD_TARGET);
            System.out.println("[系统配置] 图片的缩略图宽高尺寸 (px): " + UPLOAD_THUMB_SIZE);


            // target: 上传目标 (-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云)
            if (UPLOAD_TARGET.get() == 1) {

                // 查询分块集合 (etag)
                Map<String, Object> partList = listParts(upload_id, sysFileEntity.getObject_key());
                PartListing partListing = (PartListing) partList.get("listParts");
                List<PartSummary> partSummaryList = partListing.getParts();
                List<PartETag> etags = partSummaryList.stream()
                    .map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag()))
                    .collect(Collectors.toList());

                // 如果分块数量对不上，最好还是重新上传
                if (sysFileEntity.getUpload_chunk_count() != etags.size()) throw new CustException("分块索引异常，请重新上传");

                // [腾讯云] 合并分块
                CompleteMultipartUploadResult completeResult = tencentCosService.completeMultipartUpload(upload_id, sysFileEntity.getObject_key(), etags, is_watermark);
                if (completeResult == null) throw new CustException("分块合并失败");

                // 拼接图片路径
                sysFileEntity.setUrl(TENCENT_ACCESSIBLE_DOMAIN + "/" + completeResult.getKey());
                sysFileEntity.setRequest_id(completeResult.getRequestId());
            }

            // 3: 抖音云
            if (UPLOAD_TARGET.get() == 3) {

                CompleteMultipartUploadV2Output completeResult = douyinTosService.completeMultipartUpload(upload_id, sysFileEntity.getObject_key());
                if (completeResult == null) throw new CustException("分块合并失败");

                // 拼接图片路径
                sysFileEntity.setUrl(DOUYIN_ACCESSIBLE_DOMAIN + "/" + completeResult.getKey());
                sysFileEntity.setRequest_id(completeResult.getRequestInfo().getRequestId());

            }

            /*
                如果是图片类型，就设置封面：
                图片Icon (png / jpg / jpeg / gif / bmp / webp)
                视频 Icon (mp4 / avi / mov / flv / 3gp / mpeg / wmv)
                Word Icon (doc / docx)
                Excel Icon (xls / xlsx)
                PDF Icon (pdf)
                其他 Icon
             */
            setThumbUrl(sysFileEntity, UPLOAD_THUMB_SIZE.get(), UPLOAD_THUMB_SIZE.get(), StyleEnums.THUMB_BACKGROUND.getValue());

            // [DB] 更新分块记录
            sysFileDao.updateCompleteFile(sysFileEntity);

            // -- 合并分块成功时，返回值 -------------------------------------
            // 移除 upload_id
            sysFileEntity.setUpload_id(null);
            // 翻译 储存介质
            sysFileEntity.setTarget_label(TargetEnums.targetToLabel(sysFileEntity.getTarget()));
            // 翻译 用户名
            SysUser userEntity = sysUserDao.selectById(sysFileEntity.getUser_id());
            sysFileEntity.setUsername(userEntity.getUsername());
            // ----------------------------------------------------

            return sysFileEntity;

//        } catch (InterruptedException e) { throw new RuntimeException(e);
//        } finally { lock.unlock(); }
    }

    // 查询分块上传情况
    @Override
    public Map<String, Object> listParts(String upload_id, String object_key) {

        Integer UPLOAD_TARGET = Convert.toInt(sysCommonService.getCommonByTag("UPLOAD_TARGET"));
        // target: 上传目标 (-1:本地, 1:腾讯云, 2:阿里云, 3.抖音云)
        if (UPLOAD_TARGET == 1) {
            return Map.of("listParts", tencentCosService.listParts(upload_id, object_key));
        }
        if (UPLOAD_TARGET == 3) {
            return Map.of("listParts", douyinTosService.listParts(upload_id, object_key));
        }

        return null;
    }

}
