package com.ai.trackwalker.storage;

import com.ai.trackwalker.config.RailProperties;
import io.minio.BucketExistsArgs;
import io.minio.GetObjectArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.HexFormat;

@Service
public class ModelTestStorageService {
    private static final Logger log = LoggerFactory.getLogger(ModelTestStorageService.class);

    private final RailProperties properties;
    private final MinioClient minioClient;
    private volatile boolean minioAvailable;
    private Path fallbackRoot;

    public ModelTestStorageService(RailProperties properties) {
        this.properties = properties;
        this.minioClient = MinioClient.builder()
                .endpoint(properties.getStorage().getMinioEndpoint())
                .credentials(properties.getStorage().getMinioAccessKey(), properties.getStorage().getMinioSecretKey())
                .build();
    }

    @PostConstruct
    public void initialize() {
        fallbackRoot = Path.of(properties.getStorage().getLocalFallbackPath()).toAbsolutePath().normalize();
        try {
            Files.createDirectories(fallbackRoot);
        } catch (Exception e) {
            throw new IllegalStateException("无法创建模型测试本地存储目录", e);
        }
        try {
            ensureBucket();
            minioAvailable = true;
        } catch (Exception e) {
            minioAvailable = false;
            log.warn("模型测试对象存储暂不可用，使用本地临时存储: {}", e.getMessage());
        }
    }

    public StoredObject store(String objectKey, MultipartFile file) {
        String safeKey = normalizeObjectKey(objectKey);
        String contentType = file.getContentType() == null ? "application/octet-stream" : file.getContentType();
        String checksum = sha256(file);
        long size = file.getSize();
        if (minioAvailable) {
            try (InputStream stream = file.getInputStream()) {
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(bucket())
                        .object(safeKey)
                        .stream(stream, size, -1)
                        .contentType(contentType)
                        .build());
                return new StoredObject(safeKey, "s3://" + bucket() + "/" + safeKey, checksum, size, false);
            } catch (Exception e) {
                minioAvailable = false;
                log.warn("写入对象存储失败，切换到本地临时存储: {}", e.getMessage());
            }
        }
        try {
            Path target = fallbackRoot.resolve(safeKey).normalize();
            assertWithinFallback(target);
            Files.createDirectories(target.getParent());
            try (InputStream stream = file.getInputStream()) {
                Files.copy(stream, target, StandardCopyOption.REPLACE_EXISTING);
            }
            return new StoredObject(safeKey, target.toUri().toString(), checksum, size, true);
        } catch (Exception e) {
            throw new IllegalStateException("保存测试文件失败", e);
        }
    }

    public PartObject storePart(String sessionId, String manifestId, int partIndex, MultipartFile part) {
        String safeSession = safeSegment(sessionId);
        String safeManifest = safeSegment(manifestId);
        if (partIndex < 0) {
            throw new IllegalArgumentException("分片序号不能小于 0");
        }
        try {
            Path target = partRoot(safeSession, safeManifest).resolve(partIndex + ".part").normalize();
            assertWithinFallback(target);
            Files.createDirectories(target.getParent());
            try (InputStream stream = part.getInputStream()) {
                Files.copy(stream, target, StandardCopyOption.REPLACE_EXISTING);
            }
            return new PartObject(partIndex, part.getSize(), sha256(target));
        } catch (Exception e) {
            throw new IllegalStateException("保存上传分片失败", e);
        }
    }

    public StoredObject completeParts(
            String objectKey,
            String sessionId,
            String manifestId,
            int totalParts,
            String contentType,
            String expectedSha256
    ) {
        String safeSession = safeSegment(sessionId);
        String safeManifest = safeSegment(manifestId);
        Path root = partRoot(safeSession, safeManifest);
        Path assembled = root.resolve("assembled.bin").normalize();
        assertWithinFallback(assembled);
        try {
            Files.createDirectories(root);
            try (OutputStream output = Files.newOutputStream(assembled, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
                for (int index = 0; index < totalParts; index++) {
                    Path part = root.resolve(index + ".part").normalize();
                    if (!Files.isRegularFile(part)) {
                        throw new IllegalStateException("缺少上传分片 " + index);
                    }
                    Files.copy(part, output);
                }
            }
            String checksum = sha256(assembled);
            if (expectedSha256 != null && !expectedSha256.isBlank() && !checksum.equalsIgnoreCase(expectedSha256)) {
                throw new IllegalStateException("合并文件 SHA-256 与客户端声明不一致");
            }
            StoredObject stored = storePath(objectKey, assembled, contentType, checksum);
            cleanupParts(sessionId, manifestId, totalParts);
            return stored;
        } catch (Exception e) {
            throw e instanceof IllegalStateException state ? state : new IllegalStateException("合并上传分片失败", e);
        }
    }

    public void cleanupParts(String sessionId, String manifestId, int totalParts) {
        Path root = partRoot(safeSegment(sessionId), safeSegment(manifestId));
        try {
            for (int index = 0; index < totalParts; index++) {
                Files.deleteIfExists(root.resolve(index + ".part").normalize());
            }
            Files.deleteIfExists(root.resolve("assembled.bin").normalize());
            Files.deleteIfExists(root);
        } catch (Exception e) {
            log.warn("清理上传分片失败: session={}, manifest={}, error={}", sessionId, manifestId, e.getMessage());
        }
    }

    public byte[] read(String objectKey, String storageUrl) {
        String safeKey = normalizeObjectKey(objectKey);
        if (storageUrl != null && storageUrl.startsWith("file:")) {
            return readLocal(safeKey);
        }
        try (InputStream stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket()).object(safeKey).build())) {
            return stream.readAllBytes();
        } catch (Exception e) {
            return readLocal(safeKey);
        }
    }

    public boolean isMinioAvailable() {
        return minioAvailable;
    }

    public void delete(String objectKey, String storageUrl) {
        String safeKey = normalizeObjectKey(objectKey);
        if (storageUrl != null && storageUrl.startsWith("file:")) {
            deleteLocal(safeKey);
            return;
        }
        try {
            minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket()).object(safeKey).build());
        } catch (Exception e) {
            Path local = fallbackRoot.resolve(safeKey).normalize();
            if (Files.exists(local)) {
                deleteLocal(safeKey);
                return;
            }
            throw new IllegalStateException("删除测试对象失败", e);
        }
    }

    private byte[] readLocal(String objectKey) {
        try {
            Path target = fallbackRoot.resolve(objectKey).normalize();
            assertWithinFallback(target);
            return Files.readAllBytes(target);
        } catch (Exception e) {
            throw new IllegalStateException("读取测试文件失败", e);
        }
    }

    private void deleteLocal(String objectKey) {
        try {
            Path target = fallbackRoot.resolve(objectKey).normalize();
            assertWithinFallback(target);
            Files.deleteIfExists(target);
        } catch (Exception e) {
            throw new IllegalStateException("删除本地测试文件失败", e);
        }
    }

    private void ensureBucket() throws Exception {
        if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket()).build())) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket()).build());
        }
    }

    private StoredObject storePath(String objectKey, Path source, String contentType, String checksum) throws Exception {
        String safeKey = normalizeObjectKey(objectKey);
        long size = Files.size(source);
        if (minioAvailable) {
            try (InputStream stream = Files.newInputStream(source)) {
                minioClient.putObject(PutObjectArgs.builder()
                        .bucket(bucket())
                        .object(safeKey)
                        .stream(stream, size, -1)
                        .contentType(contentType == null ? "application/octet-stream" : contentType)
                        .build());
                return new StoredObject(safeKey, "s3://" + bucket() + "/" + safeKey, checksum, size, false);
            } catch (Exception e) {
                minioAvailable = false;
                log.warn("写入对象存储失败，切换到本地临时存储: {}", e.getMessage());
            }
        }
        Path target = fallbackRoot.resolve(safeKey).normalize();
        assertWithinFallback(target);
        Files.createDirectories(target.getParent());
        Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
        return new StoredObject(safeKey, target.toUri().toString(), checksum, size, true);
    }

    private Path partRoot(String sessionId, String manifestId) {
        Path root = fallbackRoot.resolve("_parts").resolve(sessionId).resolve(manifestId).normalize();
        assertWithinFallback(root);
        return root;
    }

    private String safeSegment(String value) {
        if (value == null || !value.matches("[A-Za-z0-9_-]{1,96}")) {
            throw new IllegalArgumentException("非法分片存储标识");
        }
        return value;
    }

    private String bucket() {
        String configured = properties.getStorage().getTestBucket();
        return configured == null || configured.isBlank() ? "rail-model-tests" : configured;
    }

    private String normalizeObjectKey(String objectKey) {
        String normalized = objectKey == null ? "" : objectKey.replace('\\', '/');
        while (normalized.startsWith("/")) {
            normalized = normalized.substring(1);
        }
        if (normalized.isBlank() || normalized.contains("../") || normalized.equals("..")) {
            throw new IllegalArgumentException("非法对象存储路径");
        }
        return normalized;
    }

    private void assertWithinFallback(Path target) {
        if (!target.startsWith(fallbackRoot)) {
            throw new IllegalArgumentException("非法本地存储路径");
        }
    }

    private String sha256(MultipartFile file) {
        try (InputStream stream = file.getInputStream()) {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] buffer = new byte[64 * 1024];
            int read;
            while ((read = stream.read(buffer)) != -1) {
                digest.update(buffer, 0, read);
            }
            return HexFormat.of().formatHex(digest.digest());
        } catch (Exception e) {
            throw new IllegalStateException("计算文件校验和失败", e);
        }
    }

    private String sha256(Path path) {
        try (InputStream stream = Files.newInputStream(path)) {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] buffer = new byte[64 * 1024];
            int read;
            while ((read = stream.read(buffer)) != -1) {
                digest.update(buffer, 0, read);
            }
            return HexFormat.of().formatHex(digest.digest());
        } catch (Exception e) {
            throw new IllegalStateException("计算分片校验和失败", e);
        }
    }

    public record StoredObject(String objectKey, String storageUrl, String sha256, long sizeBytes, boolean localFallback) {
    }

    public record PartObject(int partIndex, long sizeBytes, String sha256) {
    }
}
