package com.philisense.www.cloud;

import cn.hutool.core.util.CharUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.CredentialsProvider;
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
import com.aliyun.oss.model.*;
import com.philisense.www.config.OssStorageProperties;
import com.philisense.www.constant.StorageRedisKeyConstants;
import com.philisense.www.enums.SliceTypeEnum;
import com.philisense.www.exception.StorageException;
import com.philisense.www.rule.OssRule;
import com.philisense.www.utils.FileUtils;
import com.philisense.www.utils.SpringContextUtils;
import com.philisense.www.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 阿里云oss
 *
 * @description: 阿里云oss
 **/
public class AliyunStorageService extends OssStorageService {

    private static Logger log = LogManager.getLogger(AliyunStorageService.class);

    private final OssStorageProperties.AliyunConfig aliyunConfig;

    private OSSClient ossClient;
    private String diskName;

    private OssRule ossRule;

    private StringRedisTemplate stringRedisTemplate;

    public AliyunStorageService(OssStorageProperties.AliyunConfig aliyunConfig, String diskName) {
        this.aliyunConfig = aliyunConfig;
        this.diskName = diskName;
        // 初始化
        init();
    }

    private void init() {
        // 创建ClientConfiguration。ClientConfiguration是OSSClient的配置类，可配置代理、连接超时、最大连接数等参数。
        ClientConfiguration conf = new ClientConfiguration();
        // 设置OSSClient允许打开的最大HTTP连接数，默认为1024个。
        conf.setMaxConnections(1024);
        // 设置Socket层传输数据的超时时间，默认为50000毫秒。
        conf.setSocketTimeout(50000);
        // 设置建立连接的超时时间，默认为50000毫秒。
        conf.setConnectionTimeout(50000);
        // 设置从连接池中获取连接的超时时间（单位：毫秒），默认不超时。
        conf.setConnectionRequestTimeout(1000);
        // 设置连接空闲超时时间。超时则关闭连接，默认为60000毫秒。
        conf.setIdleConnectionTime(60000);
        // 设置失败请求重试次数，默认为3次。
        conf.setMaxErrorRetry(5);
        CredentialsProvider credentialsProvider = new DefaultCredentialProvider(aliyunConfig.getAccessKey(), aliyunConfig.getAccessSecret());
        ossClient = new OSSClient(aliyunConfig.getEndpoint(), credentialsProvider, conf);
        stringRedisTemplate = SpringContextUtils.getBean(StringRedisTemplate.class);
        ossRule = SpringContextUtils.getBean(OssRule.class);
    }

    @Override
    public Map<String, Object> upload(MultipartFile file, String path) {
        try {
            return put(aliyunConfig.getBucketName(), file.getInputStream(), path, false);
        } catch (Exception e) {
            log.error("上传文件失败：{} | {}", e.getMessage(), e);
            throw new StorageException("上传文件失败", e);
        }
    }

    @Override
    public Map<String, Object> upload(byte[] data, String path) {
        try {
            return put(aliyunConfig.getBucketName(), new ByteArrayInputStream(data), path, false);
        } catch (Exception e) {
            log.error("上传文件失败：{} | {}",e.getMessage(), e);
            throw new StorageException("上传文件失败", e);
        }
    }

    private Map<String, Object> put(String bucketName, InputStream stream, String path, boolean cover) {
        try {
            path = ossRule.filePath(diskName + FileUtils.SLASH + path);
            // 覆盖上传
            if (cover) {
                ossClient.putObject(ossRule.bucketName(bucketName), path, stream);
            } else {
                PutObjectResult response = ossClient.putObject(bucketName, path, stream);
                int retry = 0;
                int retryCount = 5;
                while (StringUtils.isEmpty(response.getETag()) && retry < retryCount) {
                    response = ossClient.putObject(bucketName, path, stream);
                    retry++;
                }
            }

            Map<String, Object> fileMap = new HashMap<>();
            fileMap.put("path", path);
            fileMap.put("url", getUrl(path));
            return fileMap;
        } catch (Exception e) {
            log.error("上传文件失败：{} | {}",e.getMessage(), e);
            throw new StorageException("上传文件失败", e);
        }
    }

    @Override
    public InputStream download(String path) {
        try {
            // ossObject包含文件所在的存储空间名称、文件名称、文件元信息以及一个输入流。
            OSSObject ossObject = ossClient.getObject(aliyunConfig.getBucketName(), diskName + FileUtils.SLASH + path);
            return ossObject.getObjectContent();
        } catch (Exception e) {
            log.error("下载文件失败：{} | {}",e.getMessage(), e);
            throw new StorageException("下载文件失败", e);
        }
    }

    @Override
    public void delete(String path) {
        try {
            ossClient.deleteObject(aliyunConfig.getBucketName(), diskName + FileUtils.SLASH + path);
        } catch (Exception e) {
            log.error("删除文件失败：{} | {}",e.getMessage(), e);
            throw new StorageException("删除文件失败", e);
        }
    }

    @Override
    public String getUrl(String path) {
        String url = "";
        try {
            //设置链接时效60分钟
            Calendar c = Calendar.getInstance();
            c.setTime(new Date());
            c.add(Calendar.MINUTE, 60);

            Date date = c.getTime();
            GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(aliyunConfig.getBucketName(), diskName + FileUtils.SLASH + path, HttpMethod.GET);
            request.addHeader("policy", "");
            request.setExpiration(date);

            Map<String, String> map = new HashMap<String, String>();
            map.put("response-content-disposition", "attachment");
            request.setQueryParameter(map);
            url = ossClient.generatePresignedUrl(request).toString();
            log.info("生成下载的url: {}", url);
        } catch (Exception e) {
            log.error("获取文件地址失败：{} | {}",e.getMessage(), e);
            throw new StorageException("获取文件地址失败", e);
        }
        return url;
    }

    @Override
    public Map<String, Object> chunksCheck(String path, String name, String hash, Integer fileSize, Integer total) {
        Map<String, Object> result = new HashMap<>();
        try {
            Integer sum = 0;

            // 设置文件路径和名称
            String fileUrl = path + CharUtil.SLASH + name;

            // 创建InitiateMultipartUploadRequest对象。
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(aliyunConfig.getBucketName(), fileUrl);

            // 如果需要在初始化分片时设置文件存储类型，请参考以下示例代码。
            // ObjectMetadata metadata = new ObjectMetadata();
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // request.setObjectMetadata(metadata);

            if (ossClient.doesObjectExist(aliyunConfig.getBucketName(), fileUrl)) {
                // 文件已存在
                result.put("type", SliceTypeEnum.UPLOADSPEED.getValue());
                result.put("msg", SliceTypeEnum.UPLOADSPEED.getText());
            }else {
                // 检查是否有数据
                String uploadId = stringRedisTemplate.opsForValue().get(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_HASH_ID, hash));
                if (StringUtils.isBlank(uploadId)) {
                    // 初始化分片。
                    InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
                    // 返回uploadId，它是分片上传事件的唯一标识，您可以根据这个uploadId发起相关的操作，如取消分片上传、查询分片上传等。
                    uploadId = upresult.getUploadId();
                    //将uploadId缓存到redis,2个小时有效
                    stringRedisTemplate.opsForValue().set(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_HASH_ID, hash), uploadId, 120 * 60 * 1000, TimeUnit.MINUTES);

                }
                ListPartsRequest listPartsRequest = new ListPartsRequest(aliyunConfig.getBucketName(), fileUrl, uploadId);
                //listParts 方法获取某个上传事件所有已上传的块
                PartListing partListing = ossClient.listParts(listPartsRequest);
                sum = partListing.getParts().size();
                if (sum > 0) {
                    if (sum.equals(total)) {
                        // 切片上传完了，没有合并
                        result.put("type", SliceTypeEnum.NOMERGE.getValue());
                        result.put("msg", SliceTypeEnum.NOMERGE.getText());
                    } else {
                        // 切片没有上传完
                        result.put("type", SliceTypeEnum.BREAKPOINTRESUME.getValue());
                        result.put("sum", sum);
                        result.put("msg", SliceTypeEnum.BREAKPOINTRESUME.getText());
                    }
                } else {
                    // 没有这个文件的切片和文件
                    result.put("type", SliceTypeEnum.NEVERUPLOAD.getValue());
                    result.put("msg", SliceTypeEnum.NEVERUPLOAD.getText());
                }

                //将uploadId缓存到redis,2个小时有效
                stringRedisTemplate.opsForValue().set(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_UPLOAD_ID, uploadId), fileUrl, 120 * 60 * 1000, TimeUnit.MINUTES);
                result.put("uploadId", uploadId);
            }
        } catch (Exception e) {
            log.error("大文件上传检查文件失败：{} | {}",e.getMessage(), e);
            throw new StorageException("大文件上传检查文件失败", e);
        }

        return result;
    }

    @Override
    public Map<String, Object> chunksUpload(MultipartFile file, String path, String hash, String uploadId, Integer index, String fileSize) {
        Map<String, Object> result = new HashMap<>();
        try {
            InputStream inStream = file.getInputStream();
            long curPartSize = file.getSize();
            String objectName = stringRedisTemplate.opsForValue().get(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_UPLOAD_ID, uploadId));
            if(StringUtils.isBlank(objectName)) {
                return null;
            }

            String bucketName = aliyunConfig.getBucketName();
            // 判断容器是否存在,不存在就创建
            if (!ossClient.doesBucketExist(bucketName)) {
                ossClient.createBucket(bucketName);
                CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
                createBucketRequest.setCannedACL(CannedAccessControlList.PublicRead);
                ossClient.createBucket(createBucketRequest);
            }

            UploadPartRequest uploadPartRequest = new UploadPartRequest();
            uploadPartRequest.setBucketName(bucketName);
            uploadPartRequest.setKey(objectName);
            uploadPartRequest.setUploadId(uploadId);
            uploadPartRequest.setInputStream(inStream);
            // 设置分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
            uploadPartRequest.setPartSize(curPartSize);
            // 设置分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出这个范围，OSS将返回InvalidArgument的错误码。
            uploadPartRequest.setPartNumber(index);
            // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
            UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
            // 每次上传分片之后，OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
            PartETag partETag = uploadPartResult.getPartETag();
            //需要将PartETag缓存存起来，如果是第一个块，需要先将创建list，以便add
            String partETagStr = stringRedisTemplate.opsForValue().get(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_PART_ETAGS, uploadId));
            List<PartETag> partETagList = null;
            if (StringUtils.isNotBlank(partETagStr)){
                partETagList = JSONObject.parseArray(partETagStr).toJavaList(PartETag.class);
            }
            if (partETagList == null){
                partETagList = new ArrayList<PartETag>();;
            }
            partETagList.add(partETag);
            stringRedisTemplate.opsForValue().set(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_PART_ETAGS, uploadId), JSONObject.toJSONString(partETagList),120*60*1000, TimeUnit.MINUTES);
            String md5Str = partETag.getETag();
            result.put("md5Str", md5Str);
        } catch (Exception e) {
            log.error("上传切片失败：{} | {}",e.getMessage(), e);
            throw new StorageException("上传切片失败", e);
        }
        return result;
    }

    @Override
    public Map<String, Object> chunksMerge(String path, String name, String hash, String uploadId, String fileSize) {
        String url = "";
        try {
            //获取该uploadId缓存的信息和各块的信息，
            String objectName = stringRedisTemplate.opsForValue().get(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_UPLOAD_ID, uploadId));
            String partETagStr = stringRedisTemplate.opsForValue().get(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_PART_ETAGS, uploadId));
            List<PartETag> partETagList = null;
            if (StringUtils.isNotBlank(partETagStr)){
                partETagList = JSONObject.parseArray(partETagStr).toJavaList(PartETag.class);
            }
            // 创建CompleteMultipartUploadRequest对象。
            // 在执行完成分片上传操作时，需要提供所有有效的partETags。OSS收到提交的partETags后，会逐一验证每个分片的有效性。当所有的数据分片验证通过后，OSS将把这些分片组合成一个完整的文件。
            CompleteMultipartUploadRequest completeMultipartUploadRequest =
                    new CompleteMultipartUploadRequest(aliyunConfig.getBucketName(), objectName, uploadId, partETagList);

            // 如果需要在完成文件上传的同时设置文件访问权限，请参考以下示例代码。
            // completeMultipartUploadRequest.setObjectACL(CannedAccessControlList.PublicRead);

            // 完成上传。
            CompleteMultipartUploadResult completeMultipartUploadResult = ossClient.completeMultipartUpload(completeMultipartUploadRequest);

            // 关闭OSSClient，删除redis缓存的内容。
            ossClient.shutdown();
            stringRedisTemplate.delete(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_HASH_ID, hash));
            stringRedisTemplate.delete(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_UPLOAD_ID, uploadId));
            stringRedisTemplate.delete(StrUtil.format(StorageRedisKeyConstants.STORAGE_UPLOAD_PART_ETAGS, uploadId));
            url = completeMultipartUploadResult.getLocation();
        } catch (Exception e) {
            log.error("合并切片失败：{} | {}",e.getMessage(), e);
            throw new StorageException("合并切片失败", e);
        }

        Map<String, Object> fileMap = new HashMap<>();
        fileMap.put("path", path);
        fileMap.put("url", url);
        return fileMap;
    }

}
