package com.njzscloud.common.oss.util;

import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.google.common.collect.HashMultimap;
import com.njzscloud.common.core.ex.Exceptions;
import com.njzscloud.common.core.tuple.Tuple2;
import com.njzscloud.common.oss.config.OSSProperties;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.ListPartsResult;
import io.minio.messages.Part;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class Minio {
    private static final MinioAsyncClient CLIENT;
    private static final Map<Class<? extends Exception>, String> ERROR_MAP = new HashMap<>();
    private static final String BUCKET_NAME;

    static {
        ERROR_MAP.put(ErrorResponseException.class, "文件存储服务响应失败");
        ERROR_MAP.put(InsufficientDataException.class, "文件读取失败");
        ERROR_MAP.put(InternalException.class, "文件服务器错误");
        ERROR_MAP.put(InvalidKeyException.class, "缺少 HMAC SHA-256 依赖");
        ERROR_MAP.put(InvalidResponseException.class, "服务器未响应");
        ERROR_MAP.put(IOException.class, "文件读取失败");
        ERROR_MAP.put(NoSuchAlgorithmException.class, "缺少 MD5 或 SHA-256 依赖");
        ERROR_MAP.put(XmlParserException.class, "数据解析失败");
        CLIENT = SpringUtil.getBean(MinioAsyncClient.class);
        BUCKET_NAME = SpringUtil.getBean(OSSProperties.class).getMinio().getBucketName();
        if (StrUtil.isNotBlank(BUCKET_NAME)) {
            createBucket(BUCKET_NAME);
        }
    }

    public static void createBucket(String bucketName) {
        Assert.notBlank(bucketName, "未指明存储位置");
        try {
            boolean exists = Minio.execSync(() -> CLIENT.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()));
            if (exists) return;
            Minio.execSync(() -> CLIENT.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()));
        } catch (Exception e) {
            log.error("存储桶创建失败", e);
        }
    }

    public static Tuple2<String, List<String>> startMltUpload(String objectName, String contentType, int partNum) {
        HashMultimap<String, String> header = HashMultimap.create();
        header.put("Content-Type", contentType);
        return Minio.execSync(() -> CLIENT
                .createMultipartUploadAsync(BUCKET_NAME, null, objectName, header, null)
                .thenApply(it -> {
                    String uploadId = it.result().uploadId();
                    LinkedList<String> urls = new LinkedList<>();
                    for (int i = 0; i < partNum; i++) {
                        int partNumber = i + 1;
                        String url = obtainPresignedUrl(objectName + "." + partNumber, MapUtil.<String, String>builder()
                                .put("partNumber", Integer.toString(partNumber))
                                .put("uploadId", uploadId)
                                .build());
                        urls.add(url);
                    }
                    return Tuple2.create(uploadId, urls);
                }));
    }

    public static void endMltUpload(String objectName, String uploadId, int partNum) {
        CompletableFuture.runAsync(() -> {
            for (int i = 0; i < partNum % 1000 + 1; i++) {
                int partNumberMarker = i + 1;
                execSync(() -> CLIENT.listPartsAsync(BUCKET_NAME,
                                null,
                                objectName,
                                null,
                                partNumberMarker,
                                uploadId,
                                null, null)
                        .thenAccept(it -> {
                            ListPartsResult result = it.result();
                            List<Part> parts = result.partList();
                            execSync(() -> CLIENT.completeMultipartUploadAsync(BUCKET_NAME,
                                    null,
                                    objectName,
                                    uploadId,
                                    parts.toArray(new Part[0]),
                                    null,
                                    null
                            ));
                        })
                );
            }
        });
    }

    public static String obtainPresignedUrl(String objectName, Map<String, String> extraQueryParams) {
        return exec(() -> CLIENT.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                .method(Method.PUT)
                .bucket(BUCKET_NAME)
                .object(objectName)
                .expiry(1, TimeUnit.DAYS)
                .extraQueryParams(extraQueryParams)
                .build()));
    }

    public static Map<String, String> obtainPresignedUrl(String objectName) {
        return exec(() -> {
            PostPolicy policy = new PostPolicy(BUCKET_NAME, ZonedDateTime.now().plusDays(1));
            policy.addEqualsCondition("key", objectName);
            Map<String, String> data = CLIENT.getPresignedPostFormData(policy);
            data.put("key", objectName);
            data.put("bucketName", BUCKET_NAME);
            data.put("objectName", objectName);
            return data;
        });
    }

    public static Tuple2<InputStream, String> obtainFile(String bucketName, String objectName) {
        GetObjectResponse response = execSync(() -> CLIENT.getObject(GetObjectArgs.builder()
                .object(objectName)
                .bucket(bucketName).build()));
        return Tuple2.create(response, response.headers().get("Content-Type"));
    }

    private static <T> T execSync(MinioExec<CompletableFuture<T>> fn) {
        try {
            return fn.exec().get();
        } catch (Exception e) {
            log.error("执行失败，文件服务器错误", e);
            throw Exceptions.error(e, ERROR_MAP.getOrDefault(e.getClass(), "文件服务器错误"));
        }
    }

    private static <T> T exec(MinioExec<T> fn) {
        try {
            return fn.exec();
        } catch (Exception e) {
            log.error("执行失败，文件服务器错误", e);
            throw Exceptions.error(e, ERROR_MAP.getOrDefault(e.getClass(), "文件服务器错误"));
        }
    }

    public static String uploadFile(String objectName, String contentType, InputStream inputStream) {
        Minio.exec(() -> CLIENT.putObject(PutObjectArgs.builder()
                .bucket(BUCKET_NAME)
                .object(objectName)
                .contentType(contentType)
                .stream(inputStream, -1, 1024 * 1024)
                .build()));
        return "/" + BUCKET_NAME + "/" + objectName;
    }

    public interface MinioExec<T> {
        T exec() throws Exception;
    }
}
