package com.backendsys.modules.sdk.douyincloud.tos.service.impl;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.backendsys.exception.CustException;
import com.backendsys.modules.common.config.security.utils.SecurityUtil;
import com.backendsys.modules.common.utils.CommonUtil;
import com.backendsys.modules.sdk.douyincloud.tos.service.DouyinTosService;
import com.backendsys.modules.sdk.tencentcloud.cos.entity.Progress;
import com.backendsys.modules.sse.entity.SseResponse;
import com.backendsys.modules.sse.entity.SseResponseEnum;
import com.backendsys.modules.sse.utils.SseUtil;
import com.backendsys.modules.upload.entity.SysFileResult;
import com.qcloud.cos.exception.CosClientException;
import com.qcloud.cos.model.PartETag;
import com.volcengine.tos.TOSV2;
import com.volcengine.tos.TOSV2ClientBuilder;
import com.volcengine.tos.TosClientException;
import com.volcengine.tos.TosServerException;
import com.volcengine.tos.comm.event.DataTransferListener;
import com.volcengine.tos.comm.event.DataTransferStatus;
import com.volcengine.tos.comm.event.DataTransferType;
import com.volcengine.tos.model.object.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

// 对象接口 -> 上传对象
// https://www.volcengine.com/docs/6349/79898
@Service
public class DouyinTosServiceImpl implements DouyinTosService {

    @Autowired
    private SseUtil sseUtil;

    @Value("${spring.application.name}")
    private String APPLICATION_NAME;
    @Value("${douyin.tos.region}")
    private String REGION;
    @Value("${douyin.tos.endpoint}")
    private String ENDPOINT;
    @Value("${douyin.tos.domain}")
    private String DOMAIN;
    @Value("${douyin.tos.bucket-name}")
    private String BUCKET_NAME;
    @Value("${douyin.tos.access-key-id}")
    private String ACCESS_KEY_ID;
    @Value("${douyin.tos.secret-access-key}")
    private String SECRET_ACCESS_KEY;


    // [抖音云COS] 获取进度函数
    private DataTransferListener getDataTransferListener(String filename, Long input_user_id) {

        return new DataTransferListener() {
            // 自定义实现 DataTransferListener 的 dataTransferStatusChange 接口
            @Override
            public void dataTransferStatusChange(DataTransferStatus dataTransferStatus) {

                long once = dataTransferStatus.getRwOnceBytes();
                long current = dataTransferStatus.getConsumedBytes();
                long total = dataTransferStatus.getTotalBytes();
                String state = "";
                BigDecimal percentage = Convert.toBigDecimal(0);

                if (dataTransferStatus.getType() == DataTransferType.DATA_TRANSFER_STARTED) {
                    state = "init";
                    System.out.println("putObject started.");
                } else if (dataTransferStatus.getType() == DataTransferType.DATA_TRANSFER_RW) {
                    state = "inprogress";
                    // 计算百分比
                    percentage = NumberUtil.round(NumberUtil.mul(current, 100) / total, 2);
                    System.out.printf("putObject progress: [%d / %d] = %.02f%% \n", current, total, percentage);
                } else if (dataTransferStatus.getType() == DataTransferType.DATA_TRANSFER_FAILED) {
                    state = "completed";
                    System.out.printf("putObject failed, has sent %d bytes, total %d bytes.\n", dataTransferStatus.getConsumedBytes(), total);
                } else if (dataTransferStatus.getType() == DataTransferType.DATA_TRANSFER_SUCCEED) {
                    state = "completed";
                    System.out.printf("putObject succeed, has sent %d bytes, total %d bytes.\n", dataTransferStatus.getConsumedBytes(), total);
                }

                // [SSE] 进度回传
                Progress progress = new Progress();
                progress.setFilename(filename);
                progress.setCurrent(current);
                progress.setTotal(total);
                progress.setPercent(percentage);
                progress.setState(state);
                String dataStr = (new SseResponse(SseResponseEnum.UPLOAD, progress)).toJsonStr();

                Long user_id = input_user_id == null ? SecurityUtil.getUserId() : input_user_id;

                sseUtil.send(user_id, dataStr);

            }
        };
    }

    /**
     * [抖音云TOS] 上传对象
     */
    @Override
    public SysFileResult putObject(MultipartFile multipartFile, String object_key) {

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

        // 生成 UUID 文件名
        String filename = CommonUtil.generateUUIDFilename(multipartFile);

        // 如果不传 object_key，则默认使用临时文件夹 (/temp/UUID文件名)
        if (StrUtil.isEmpty(object_key)) object_key = "temp/" + filename;
        System.out.println("Upload object: " + object_key);

        // ------------------------------------------------------------------------------

        InputStream inputStream = null;

        // 初始化 TosClient
        TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);

        // TOSV2 提供的所有接口均会抛出 TosException 异常，需要使用 try-catch 进行捕获并处理。
        try{

            // 设置上传的桶名和对象名
            inputStream = multipartFile.getInputStream();
            PutObjectInput putObjectInput = new PutObjectInput()
                .setBucket(BUCKET_NAME)
                .setKey(object_key)
                .setContent(inputStream);

            // 以下代码展示如何处理进度条
            // 在 ObjectMetaRequestOptions 中设置文件大小，可在进度条中显示 total 总长度，否则 DataTransferStatus.getTotalBytes 值为 -1。
            ObjectMetaRequestOptions options = new ObjectMetaRequestOptions().setContentLength(multipartFile.getSize());
            putObjectInput.setOptions(options);

            // 自定义实现 DataTransferListener，实现进度条功能
            DataTransferListener listener = getDataTransferListener(filename, null);
            putObjectInput.setDataTransferListener(listener);

            // 上传对象
            PutObjectOutput output = tos.putObject(putObjectInput);

            // 自定义返回结果实体
            SysFileResult result = new SysFileResult();
            result.setKey(object_key);
            result.setRequest_id(output.getRequestInfo().getRequestId());
            result.setE_tag(output.getEtag());
            result.setDomain(DOMAIN);
            result.setUrl(DOMAIN + "/" + object_key);
            result.setTarget(3);
            return result;

        } catch (TosClientException | TosServerException e) {
            // 操作失败，捕获客户端异常，一般情况是请求参数错误，此时请求并未发送
            System.out.println("putObject failed (TosClientException / TosServerException)");
            throw new CustException(e.getMessage());
        } catch (Throwable t) {
            // 作为兜底捕获其他异常，一般不会执行到这里
            System.out.println("putObject failed (Throwable)");
            throw new CustException(t.getMessage());
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // 忽略关闭流时的异常
                }
            }
            try {
                tos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * [抖音云COS] 删除对象
     */
    @Override
    public void deleteObject(String object_key) {
        try {
            // 初始化 TosClient
            TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
            DeleteObjectInput input = new DeleteObjectInput().setBucket(BUCKET_NAME).setKey(object_key);
            tos.deleteObject(input);
            System.out.println("deleteObject succeed.");
            tos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // [抖音云TOS] 批量删除对象
    @Override
    public void deleteObjects(List<String> object_keys) {
        try {

            // 初始化 TosClient
            TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);

            List<ObjectTobeDeleted> objs = new ArrayList<>();
            object_keys.stream().forEach(object_key -> {
                ObjectTobeDeleted obj = new ObjectTobeDeleted().setKey(object_key);
                objs.add(obj);
            });

            System.out.println("[抖音云TOS][即将删除对象]: " +
                    object_keys.stream().collect(Collectors.joining(", ", "[", "]")));

            DeleteMultiObjectsV2Input input = new DeleteMultiObjectsV2Input().setBucket(BUCKET_NAME).setObjects(objs);
            DeleteMultiObjectsV2Output output = tos.deleteMultiObjects(input);
            System.out.println("[抖音云TOS][批量删除对象成功].");

            if (output.getDeleteds() != null) {
                for (int i = 0; i < output.getDeleteds().size(); i++){
                    Deleted deleted = output.getDeleteds().get(i);
                    System.out.println("deleted object is " + deleted);
                }
            }
            if (output.getErrors() != null) {
                for (int i = 0; i < output.getErrors().size(); i++){
                    DeleteError error = output.getErrors().get(i);
                    System.out.println("delete error is " + error);
                }
            }

            tos.close();

        } catch (TosClientException e) {
            System.out.println("deleteMultiObjects failed (TosClientException): " + e.getMessage());
        } catch (TosServerException e) {
            System.out.println("deleteMultiObjects failed (TosServerException): " + e.getMessage());
        } catch (Throwable t) {
            // 作为兜底捕获其他异常，一般不会执行到这里
            System.out.println("deleteMultiObjects failed");
            System.out.println("unexpected exception, message: " + t.getMessage());
        }
    }


    /**
     * [抖音云TOS] 初始化分块上传
     */
    @Override
    public CreateMultipartUploadOutput initiateMultipartUpload(String object_name) {
        TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
        try {

            // 使用临时文件夹 (/temp) + UUID文件名
            String object_key = "temp/" + object_name;
            System.out.println("InitiateMultipartUpload object: " + object_key);

            // 初始化分片上传
            CreateMultipartUploadInput create = new CreateMultipartUploadInput().setBucket(BUCKET_NAME).setKey(object_key);
            CreateMultipartUploadOutput createOutput = tos.createMultipartUpload(create);
            System.out.println("CreateMultipartUpload succeed. UploadId is: " + createOutput.getUploadID());
            return createOutput;

        } catch (TosClientException | TosServerException e) {
            // 操作失败，捕获客户端异常，一般情况是请求参数错误，此时请求并未发送
            throw new CustException("TosClientException/TosServerException: " + e.getMessage());
        } catch (Throwable t) {
            // 作为兜底捕获其他异常，一般不会执行到这里
            throw new CustException("Throwable (2): " + t.getMessage());
        }
    }

    /**
     * [抖音云TOS] 上传分块
     * - 抖音云的单块上传大小不能小于 4MB（最后一块可忽略）
     */
    @Override
    public UploadPartV2Output uploadPart(MultipartFile multipartFile, String upload_id, String object_key, Integer partNumber) {
        InputStream inputStream = null;
        TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
        try {

            inputStream = multipartFile.getInputStream();
            UploadPartV2Input input = new UploadPartV2Input()
                .setBucket(BUCKET_NAME)
                .setKey(object_key)
                .setUploadID(upload_id)
                .setPartNumber(partNumber)
                .setContentLength(multipartFile.getSize())
                .setContent(inputStream);
            UploadPartV2Output output = tos.uploadPart(input);
            return output;

        } catch (IOException | CosClientException | TosClientException | TosServerException e) {
            throw new CustException(e.getMessage());
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    // 忽略关闭流时的异常
                    throw new CustException(e.getMessage());
                }
            }
        }
    }


    /**
     * [抖音云TOS] 完成分块上传
     */
    @Override
    public CompleteMultipartUploadV2Output completeMultipartUpload(String upload_id, String object_key) {

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

        TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
        try {

            CompleteMultipartUploadV2Input complete = new CompleteMultipartUploadV2Input()
                .setBucket(BUCKET_NAME)
                .setKey(object_key)
                .setUploadID(upload_id);

            complete.setCompleteAll(true);

            CompleteMultipartUploadV2Output completedOutput = tos.completeMultipartUpload(complete);
            return completedOutput;

        } catch (TosClientException e) {
            System.out.println("TosClientException.");
            throw new CustException(e.getMessage());
        } catch (TosServerException e) {
            System.out.println("TosServerException.");
            System.out.println("Code: " + e.getCode() + ", StatusCode: " + e.getStatusCode());

            if ("EntityTooSmall".equals(e.getCode())) {
                // 错误原因：上传的Object超出允许的最小值
                // Your proposed upload is smaller than the minimum allowed size
                // 分片编号从 1 开始，最大为 10000。除最后一个分片以外，其他分片大小最小为 4MiB
            }
            throw new CustException(e.getMessage());
        } catch (Throwable t) {
            throw new CustException(t.getMessage());
        }

    }


    /**
     * [抖音云TOS] 查询已上传的分块
     */
    @Override
    public List<ListPartsOutput> listParts(String upload_id, String object_key) {

        TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
        try {

            List<ListPartsOutput> list = new ArrayList<>();
            // maxParts 设置返回的记录最大条数
            int maxParts = 1000;
            // isTruncated 为 true 代表后续还有数据，为 false 代表已经列举完全部数据
            boolean isTruncated = true;
            int partNumberMarker = 0;
            int total = 0;
            while(isTruncated) {
                ListPartsInput input = new ListPartsInput().setBucket(BUCKET_NAME).setKey(object_key)
                    // 必须设置 bucket, key, uploadId
                    .setUploadID(upload_id).setPartNumberMarker(partNumberMarker).setMaxParts(maxParts);
                ListPartsOutput output = tos.listParts(input);

                list.add(output);
//                System.out.printf("listParts succeed, is truncated? %b, next partNumber marker is %d \n", output.isTruncated(), output.getNextPartNumberMarker());
//                if (output.getUploadedParts() != null) {
//                    for (int i = 0; i < output.getUploadedParts().size(); i++) {
//                        UploadedPartV2 upload = output.getUploadedParts().get(i);
//                        System.out.printf("Uploaded part, partNumber is %d, etag is %s, lastModified is %s, " +
//                            "size is %d.\n", upload.getPartNumber(), upload.getEtag(), upload.getLastModified(), upload.getSize());
//                    }
//                    total += output.getUploadedParts().size();
//                }
                isTruncated = output.isTruncated();
                partNumberMarker = output.getNextPartNumberMarker();
            }
            return list;

        } catch (TosClientException e) {
            System.out.println("TosClientException.");
            throw new CustException(e.getMessage());
        } catch (TosServerException e) {
            System.out.println("TosServerException.");
            throw new CustException(e.getMessage());
        } catch (Throwable t) {
            throw new CustException(t.getMessage());
        }

    }

    /**
     * [抖音云TOS] URL 转存
     */
    @Override
    public SysFileResult urlToTOS(String origin_url) {
        return urlToTOS(origin_url, null);
    }
    @Override
    public SysFileResult urlToTOS(String origin_url, String input_suffix) {
        return urlToTOS(origin_url, input_suffix);
    }
    @Override
    public SysFileResult urlToTOS(String origin_url, String input_suffix, Long user_id) {

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

        // 下载URL文件到本地
        File downloadFile = CommonUtil.downloadLocalFromUrl(origin_url);
        // 获得URL文件后缀名
        String suffix = StrUtil.isEmpty(input_suffix) ? CommonUtil.getFileSuffixFromUrl(origin_url) : input_suffix;
        // 获得文件大小
        Long size = downloadFile.length();

        // 使用临时文件夹 (/temp) + UUID文件名.后缀名
        String object_filename = CommonUtil.generateUUIDFilename(suffix);
        String object_key = "temp/" + object_filename;
        System.out.println("initiateMultipartUpload object: " + object_key);

        // 初始化 TosClient
        TOSV2 tos = new TOSV2ClientBuilder().build(REGION, ENDPOINT, ACCESS_KEY_ID, SECRET_ACCESS_KEY);
        try (FileInputStream inputStream = new FileInputStream(downloadFile)) {

            // TOSV2 提供的所有接口均会抛出 TosException 异常，需要使用 try-catch 进行捕获并处理。
            try {

                // 设置上传的桶名和对象名
                PutObjectInput putObjectInput = new PutObjectInput()
                        .setBucket(BUCKET_NAME)
                        .setKey(object_key)
                        .setContent(inputStream);

                // 以下代码展示如何处理进度条
                // 在 ObjectMetaRequestOptions 中设置文件大小，可在进度条中显示 total 总长度，否则 DataTransferStatus.getTotalBytes 值为 -1。
                ObjectMetaRequestOptions options = new ObjectMetaRequestOptions().setContentLength(downloadFile.length());
                putObjectInput.setOptions(options);

                // 自定义实现 DataTransferListener，实现进度条功能
                DataTransferListener listener = getDataTransferListener(object_filename, user_id);
                putObjectInput.setDataTransferListener(listener);

                // 上传对象
                PutObjectOutput output = tos.putObject(putObjectInput);

                // 自定义返回结果实体
                SysFileResult result = new SysFileResult();
                result.setDomain(DOMAIN);
                result.setKey(object_key);
                result.setRequest_id(output.getRequestInfo().getRequestId());
                result.setE_tag(output.getEtag());
                result.setSize(size);
                result.setUrl(DOMAIN + "/" + object_key);
                result.setTarget(3);
                return result;

            } catch (TosClientException | TosServerException e) {
                // 操作失败，捕获客户端异常，一般情况是请求参数错误，此时请求并未发送
                System.out.println("putObject failed (TosClientException / TosServerException)");
                throw new CustException(e.getMessage());
            } catch (Throwable t) {
                // 作为兜底捕获其他异常，一般不会执行到这里
                System.out.println("putObject failed (Throwable)");
                throw new CustException(t.getMessage());
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        // 忽略关闭流时的异常
                    }
                }
                try {
                    tos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        } catch (IOException e) {
            throw new CustException(e.getMessage());
        }

    }
}
