package com.njcn.gather.aireport.common.storage;

import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.common.config.AiReportMinioProperties;
import com.njcn.gather.aireport.common.constant.AiReportFileConst;
import io.minio.BucketExistsArgs;
import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
import io.minio.ListObjectsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.Result;
import io.minio.StatObjectArgs;
import io.minio.errors.ErrorResponseException;
import io.minio.messages.Item;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;

/**
 * ai-report MinIO 存储服务。
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AiReportMinioStorageService {

    private final AiReportMinioProperties properties;

    public StoredFile saveMultipartFile(String bizDir, String ownerId, MultipartFile file, boolean keepOriginalName) {
        MultipartFile normalizedFile = requireFile(file);
        String originalFileName = normalizeFileName(normalizedFile.getOriginalFilename());
        String targetFileName = keepOriginalName ? sanitizeFileName(originalFileName) : buildStoredFileName(originalFileName);
        String objectName = buildObjectName(bizDir, ownerId, targetFileName);
        String bucketName = requireBucket();
        String contentType = resolveContentType(normalizedFile.getContentType(), originalFileName);
        try (InputStream inputStream = normalizedFile.getInputStream()) {
            putObject(bucketName, objectName, inputStream, normalizedFile.getSize(), contentType);
            return new StoredFile(bucketName, objectName, targetFileName, originalFileName, contentType, normalizedFile.getSize());
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            log.error("save multipart file to minio failed, objectName={}", objectName, exception);
            throw new BusinessException(CommonResponseEnum.FAIL, "保存文件失败");
        }
    }

    public StoredFile saveInputStream(String bizDir, String ownerId, String originalFileName, InputStream inputStream,
                                      long size, String contentType, boolean keepOriginalName) {
        String normalizedFileName = normalizeFileName(originalFileName);
        String targetFileName = keepOriginalName ? sanitizeFileName(normalizedFileName) : buildStoredFileName(normalizedFileName);
        String objectName = buildObjectName(bizDir, ownerId, targetFileName);
        String bucketName = requireBucket();
        try {
            putObject(bucketName, objectName, inputStream, size, resolveContentType(contentType, normalizedFileName));
            return new StoredFile(bucketName, objectName, targetFileName, normalizedFileName,
                    resolveContentType(contentType, normalizedFileName), size);
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            log.error("save input stream to minio failed, objectName={}", objectName, exception);
            throw new BusinessException(CommonResponseEnum.FAIL, "保存文件失败");
        }
    }

    public StoredFile saveBytes(String bizDir, String ownerId, String originalFileName, byte[] content,
                                String contentType, boolean keepOriginalName) {
        byte[] fileContent = content == null ? new byte[0] : content;
        return saveInputStream(bizDir, ownerId, originalFileName, new ByteArrayInputStream(fileContent),
                fileContent.length, contentType, keepOriginalName);
    }

    public StoredObject getObject(String objectName) {
        if (StrUtil.isBlank(objectName)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "文件不存在");
        }
        String bucketName = requireBucket();
        try {
            GetObjectResponse response = buildClient().getObject(GetObjectArgs.builder()
                    .bucket(bucketName)
                    .object(objectName)
                    .build());
            return new StoredObject(bucketName, objectName, response);
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            log.error("read file from minio failed, objectName={}", objectName, exception);
            throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
        }
    }

    public byte[] readBytes(String objectName) {
        try (StoredObject storedObject = getObject(objectName);
             InputStream inputStream = storedObject.getInputStream();
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            byte[] buffer = new byte[8192];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
            return outputStream.toByteArray();
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            log.error("read file bytes from minio failed, objectName={}", objectName, exception);
            throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
        }
    }

    public Path downloadToTempFile(String objectName, String fileName) {
        String normalizedFileName = normalizeFileName(fileName);
        String extension = resolveExtension(normalizedFileName);
        String suffix = StrUtil.isBlank(extension) ? ".tmp" : "." + extension;
        try (StoredObject storedObject = getObject(objectName);
             InputStream inputStream = storedObject.getInputStream()) {
            Path tempFile = Files.createTempFile("ai-report-", suffix);
            Files.copy(inputStream, tempFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
            return tempFile;
        } catch (BusinessException exception) {
            throw exception;
        } catch (Exception exception) {
            log.error("download minio object to temp file failed, objectName={}", objectName, exception);
            throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
        }
    }

    public List<String> listFileNames(String prefix) {
        List<String> objectNames = listObjectNames(prefix);
        List<String> result = new ArrayList<String>();
        for (String objectName : objectNames) {
            int slashIndex = objectName.lastIndexOf('/');
            result.add(slashIndex >= 0 ? objectName.substring(slashIndex + 1) : objectName);
        }
        return result;
    }

    public List<String> listObjectNames(String prefix) {
        List<String> result = new ArrayList<String>();
        if (StrUtil.isBlank(prefix)) {
            return result;
        }
        try {
            Iterable<Result<Item>> iterable = buildClient().listObjects(ListObjectsArgs.builder()
                    .bucket(requireBucket())
                    .prefix(trimPath(prefix) + "/")
                    .recursive(true)
                    .build());
            for (Result<Item> itemResult : iterable) {
                Item item = itemResult.get();
                if (item == null || item.isDir()) {
                    continue;
                }
                String objectName = item.objectName();
                if (StrUtil.isBlank(objectName)) {
                    continue;
                }
                result.add(objectName);
            }
            return result;
        } catch (Exception exception) {
            log.error("list minio objects failed, prefix={}", prefix, exception);
            throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
        }
    }

    public void deleteQuietly(String objectName) {
        if (StrUtil.isBlank(objectName)) {
            return;
        }
        try {
            buildClient().removeObject(RemoveObjectArgs.builder()
                    .bucket(requireBucket())
                    .object(objectName)
                    .build());
        } catch (Exception exception) {
            log.warn("delete file from minio failed, objectName={}", objectName, exception);
        }
    }

    public boolean exists(String objectName) {
        if (StrUtil.isBlank(objectName)) {
            return false;
        }
        try {
            buildClient().statObject(StatObjectArgs.builder()
                    .bucket(requireBucket())
                    .object(objectName)
                    .build());
            return true;
        } catch (ErrorResponseException exception) {
            String errorCode = exception.errorResponse() == null ? null : exception.errorResponse().code();
            if ("NoSuchKey".equals(errorCode) || "NoSuchObject".equals(errorCode) || "NoSuchBucket".equals(errorCode)) {
                return false;
            }
            log.error("stat minio object failed, objectName={}", objectName, exception);
            throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
        } catch (Exception exception) {
            log.error("stat minio object failed, objectName={}", objectName, exception);
            throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
        }
    }

    public void deleteTempFileQuietly(Path tempFile) {
        if (tempFile == null) {
            return;
        }
        try {
            Files.deleteIfExists(tempFile);
        } catch (IOException ignored) {
            // 临时文件清理失败不影响主流程。
        }
    }

    private void putObject(String bucketName, String objectName, InputStream inputStream, long size, String contentType) throws Exception {
        ensureBucket(buildClient(), bucketName);
        buildClient().putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .stream(inputStream, size, -1)
                .contentType(contentType)
                .build());
    }

    private MultipartFile requireFile(MultipartFile file) {
        if (file == null || file.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "上传文件不能为空");
        }
        return file;
    }

    private String requireBucket() {
        validateConfig();
        return properties.requireBucket(AiReportFileConst.BUCKET_KEY_AI_REPORT);
    }

    private void validateConfig() {
        if (StrUtil.hasBlank(properties.getEndpoint(), properties.getAccessKey(), properties.getSecretKey())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "MinIO配置不完整");
        }
    }

    private MinioClient buildClient() {
        return MinioClient.builder()
                .endpoint(properties.getEndpoint())
                .credentials(properties.getAccessKey(), properties.getSecretKey())
                .build();
    }

    private void ensureBucket(MinioClient client, String bucketName) throws Exception {
        boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        if (!exists) {
            client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    private String buildObjectName(String bizDir, String ownerId, String fileName) {
        StringBuilder builder = new StringBuilder();
        builder.append(trimPath(bizDir));
        if (StrUtil.isNotBlank(ownerId)) {
            builder.append("/").append(trimPath(ownerId));
        }
        builder.append("/").append(fileName);
        return builder.toString();
    }

    private String buildStoredFileName(String originalFileName) {
        return UUID.randomUUID().toString().replace("-", "") + "-" + sanitizeFileName(originalFileName);
    }

    private String sanitizeFileName(String originalFileName) {
        String fileName = normalizeFileName(originalFileName);
        int slashIndex = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
        String normalizedFileName = slashIndex >= 0 ? fileName.substring(slashIndex + 1) : fileName;
        return normalizedFileName.replaceAll("[\\\\/:*?\"<>|]", "_");
    }

    private String normalizeFileName(String originalFileName) {
        String fileName = StrUtil.trimToEmpty(originalFileName);
        if (StrUtil.isBlank(fileName)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "文件名不能为空");
        }
        return fileName;
    }

    private String trimPath(String value) {
        String normalized = StrUtil.trimToEmpty(value).replace("\\", "/");
        while (normalized.startsWith("/")) {
            normalized = normalized.substring(1);
        }
        while (normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        if (StrUtil.isBlank(normalized)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "对象目录配置不合法");
        }
        return normalized;
    }

    private String resolveExtension(String fileName) {
        int dotIndex = fileName.lastIndexOf('.');
        if (dotIndex < 0 || dotIndex == fileName.length() - 1) {
            return "";
        }
        return fileName.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
    }

    private String resolveContentType(String contentType, String fileName) {
        if (StrUtil.isNotBlank(contentType)) {
            return contentType;
        }
        return MediaTypeFactory.getMediaType(fileName).map(MediaType::toString)
                .orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE);
    }

    @Getter
    public static class StoredFile {
        private final String bucketName;
        private final String objectName;
        private final String storedFileName;
        private final String originalFileName;
        private final String contentType;
        private final long fileSize;

        public StoredFile(String bucketName, String objectName, String storedFileName, String originalFileName,
                          String contentType, long fileSize) {
            this.bucketName = bucketName;
            this.objectName = objectName;
            this.storedFileName = storedFileName;
            this.originalFileName = originalFileName;
            this.contentType = contentType;
            this.fileSize = fileSize;
        }
    }

    public static class StoredObject implements AutoCloseable {
        private final String bucketName;
        private final String objectName;
        private final GetObjectResponse inputStream;

        public StoredObject(String bucketName, String objectName, GetObjectResponse inputStream) {
            this.bucketName = bucketName;
            this.objectName = objectName;
            this.inputStream = inputStream;
        }

        public String getBucketName() {
            return bucketName;
        }

        public String getObjectName() {
            return objectName;
        }

        public GetObjectResponse getInputStream() {
            return inputStream;
        }

        @Override
        public void close() throws Exception {
            if (inputStream != null) {
                inputStream.close();
            }
        }
    }
}
