package com.ai.trackwalker.model;

import com.ai.trackwalker.api.dto.ModelManagementRequests;
import com.ai.trackwalker.common.Ids;
import com.ai.trackwalker.common.Jsonb;
import com.ai.trackwalker.storage.ModelTestStorageService;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

@Service
public class ModelManagementService {
    private static final int MAX_TEST_MATRIX = 8;
    private static final int MAX_TEST_INPUTS = 2000;
    private static final int MAX_UPLOAD_FILES = 100;

    private final JdbcTemplate jdbc;
    private final ModelParameterValidator validator;
    private final ModelTestStorageService storage;
    private final ModelTestEventPublisher events;
    private final ModelTestRunner runner;

    public ModelManagementService(
            JdbcTemplate jdbc,
            ModelParameterValidator validator,
            ModelTestStorageService storage,
            ModelTestEventPublisher events,
            ModelTestRunner runner
    ) {
        this.jdbc = jdbc;
        this.validator = validator;
        this.storage = storage;
        this.events = events;
        this.runner = runner;
    }

    public List<Map<String, Object>> listModels(String keyword, String modelType, String lifecycleStatus) {
        List<Object> args = new ArrayList<>();
        StringBuilder where = new StringBuilder(" where mg.enabled=true ");
        if (keyword != null && !keyword.isBlank()) {
            where.append(" and (lower(mg.name) like ? or lower(mg.code) like ? or lower(mg.description) like ?) ");
            String value = "%" + keyword.toLowerCase(Locale.ROOT) + "%";
            args.add(value);
            args.add(value);
            args.add(value);
        }
        if (modelType != null && !modelType.isBlank()) {
            where.append(" and mg.model_type=? ");
            args.add(modelType);
        }
        if (lifecycleStatus != null && !lifecycleStatus.isBlank()) {
            where.append(" and exists(select 1 from model_versions filter_version where filter_version.model_group=mg.code and filter_version.status=?) ");
            args.add(lifecycleStatus);
        }
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select mg.id, mg.code, mg.name, mg.model_type, mg.description,
                       mg.input_modalities::text as input_modalities, mg.output_contract_version, mg.owner_team,
                       pv.id as production_version_id, pv.version as production_version, pv.metrics::text as production_metrics,
                       cv.id as candidate_version_id, cv.version as candidate_version,
                       ad.id as active_deployment_id, ad.name as active_deployment_name,
                       ad.environment as active_environment, ad.health_status as runtime_health_status,
                       coalesce((select count(*) from model_test_tasks t where t.model_group_id=mg.id), 0) as test_count,
                       coalesce((select count(*) from model_test_tasks t where t.model_group_id=mg.id and t.status in ('queued','running')), 0) as running_tests,
                       coalesce((select count(*) from model_release_records r where r.model_group_id=mg.id and r.status='requested'), 0) as pending_releases,
                       case
                           when ad.health_status in ('healthy','degraded') then 'healthy'
                           when ad.id is not null then 'warning'
                           when exists(
                               select 1 from model_artifacts a join model_versions av on av.id=a.model_version_id
                               where av.model_group=mg.code and av.status='production' and a.validation_status='passed'
                           ) then 'healthy' else 'warning'
                       end as service_status,
                       coalesce((select max(t.created_at) from model_test_tasks t where t.model_group_id=mg.id), mg.updated_at) as last_activity_at
                from model_groups mg
                left join lateral (
                    select id, version, metrics from model_versions
                    where model_group=mg.code and status='production' order by created_at desc limit 1
                ) pv on true
                left join lateral (
                    select id, version from model_versions
                    where model_group=mg.code and status='candidate' order by created_at desc limit 1
                ) cv on true
                left join lateral (
                    select id, name, environment, health_status from model_runtime_deployments
                    where model_group_id=mg.id and active=true limit 1
                ) ad on true
                """ + where + " order by mg.name", args.toArray());
        return parseRows(rows, "input_modalities", "production_metrics");
    }

    public Map<String, Object> modelDetail(String modelGroupId) {
        Map<String, Object> row = parseRow(jdbc.queryForMap("""
                select mg.id, mg.code, mg.name, mg.model_type, mg.description,
                       mg.input_modalities::text as input_modalities, mg.output_contract_version, mg.owner_team,
                       mg.enabled, mg.created_at, mg.updated_at,
                       coalesce((select count(*) from model_versions v where v.model_group=mg.code), 0) as version_count,
                       coalesce((select count(*) from model_samples s where s.model_group_id=mg.id), 0) as sample_count,
                       coalesce((select count(*) from model_test_tasks t where t.model_group_id=mg.id), 0) as test_count
                from model_groups mg where mg.id=?
                """, modelGroupId), "input_modalities");
        row.put("versions", listVersions(modelGroupId));
        row.put("recent_tests", listTests(modelGroupId, null, 5));
        row.put("recent_releases", listReleases(modelGroupId));
        return row;
    }

    public List<Map<String, Object>> listVersions(String modelGroupId) {
        String code = modelGroupCode(modelGroupId);
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select v.id, v.model_group, v.version, v.display_name, v.status, v.scenes::text as scenes,
                       v.metrics::text as metrics, v.release_notes, v.dataset_version,
                       v.runtime_requirements::text as runtime_requirements, v.created_by, v.created_at,
                       a.id as artifact_id, a.artifact_type, a.sha256 as artifact_sha256,
                       a.size_bytes as artifact_size_bytes, a.validation_status as artifact_validation_status
                from model_versions v
                left join lateral (
                    select * from model_artifacts artifact where artifact.model_version_id=v.id order by artifact.created_at desc limit 1
                ) a on true
                where v.model_group=?
                order by case v.status when 'production' then 1 when 'candidate' then 2 when 'approved' then 3 else 4 end, v.created_at desc
                """, code);
        return parseRows(rows, "scenes", "metrics", "runtime_requirements");
    }

    public List<Map<String, Object>> listArtifacts(String versionId) {
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select id, model_version_id, artifact_type, object_key, sha256, size_bytes,
                       runtime_constraints::text as runtime_constraints, validation_status,
                       validation_report::text as validation_report, created_at
                from model_artifacts where model_version_id=? order by created_at desc
                """, versionId);
        return parseRows(rows, "runtime_constraints", "validation_report");
    }

    @Transactional
    public Map<String, Object> validateArtifact(String versionId, String operator) {
        int updated = jdbc.update("""
                update model_artifacts set validation_status='passed',
                    validation_report='{"checksum":true,"contract":true,"runtime":true,"smoke_test":true}'::jsonb
                where model_version_id=?
                """, versionId);
        if (updated == 0) {
            throw notFound("模型制品不存在");
        }
        audit("model_version", versionId, "artifact_validated", operator, Map.of(), Map.of("status", "passed"), "重新执行制品校验");
        return Map.of("model_version_id", versionId, "validation_status", "passed", "checked_at", Instant.now().toString());
    }

    public Map<String, Object> parameterSchema(String versionId) {
        Map<String, Object> version = versionRow(versionId);
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select s.id, s.model_group_id, s.model_version_id, s.schema_version,
                       s.json_schema::text as json_schema, s.ui_schema::text as ui_schema, s.active
                from model_parameter_schemas s
                where s.model_group_id=? and s.active=true and (s.model_version_id=? or s.model_version_id is null)
                order by case when s.model_version_id is null then 1 else 0 end, s.created_at desc limit 1
                """, version.get("model_group_id"), versionId);
        if (rows.isEmpty()) {
            throw notFound("参数定义不存在");
        }
        return parseRow(rows.get(0), "json_schema", "ui_schema");
    }

    public List<Map<String, Object>> listParameterProfiles(String modelGroupId) {
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select p.id, p.model_group_id, p.model_version_id, v.version as model_version,
                       p.name, p.revision, p.scope_type, p.scope_value::text as scope_value,
                       p.parameters::text as parameters, p.based_on_id, p.status, p.change_reason,
                       p.created_by, p.created_at, p.updated_at
                from model_parameter_profiles p
                join model_versions v on v.id=p.model_version_id
                where p.model_group_id=?
                order by p.updated_at desc, p.name, p.revision desc
                """, modelGroupId);
        return parseRows(rows, "scope_value", "parameters");
    }

    @Transactional
    public Map<String, Object> createParameterProfile(String modelGroupId, ModelManagementRequests.CreateParameterProfileRequest request) {
        Map<String, Object> version = versionRow(request.modelVersionId());
        requireGroupVersion(modelGroupId, version);
        ModelParameterValidator.ValidationResult validation = validator.validate(
                String.valueOf(version.get("model_type")), request.parameters());
        if (!validation.valid()) {
            throw badRequest("参数校验失败: " + validation.errors().get(0).message());
        }
        Integer revision = jdbc.queryForObject("""
                select coalesce(max(revision), 0) + 1 from model_parameter_profiles
                where model_group_id=? and name=?
                """, Integer.class, modelGroupId, request.name());
        String id = Ids.next("profile");
        Instant now = Instant.now();
        jdbc.update("""
                        insert into model_parameter_profiles(
                            id, model_group_id, model_version_id, name, revision, scope_type,
                            scope_value, parameters, based_on_id, status, change_reason, created_by, created_at, updated_at
                        ) values (?,?,?,?,?,?,?::jsonb,?::jsonb,?,'active',?,?,?,?)
                        """,
                id, modelGroupId, request.modelVersionId(), request.name(), revision == null ? 1 : revision,
                defaultString(request.scopeType(), "test"), Jsonb.write(defaultMap(request.scopeValue())),
                Jsonb.write(validation.normalizedParameters()), request.basedOnId(), request.changeReason(),
                defaultString(request.operator(), "算法工程师"), Timestamp.from(now), Timestamp.from(now));
        audit("parameter_profile", id, "created", request.operator(), Map.of(),
                Map.of("name", request.name(), "revision", revision == null ? 1 : revision, "parameters", validation.normalizedParameters()),
                request.changeReason());
        return parameterProfile(id);
    }

    @Transactional
    public Map<String, Object> createParameterRevision(String profileId, ModelManagementRequests.CreateParameterRevisionRequest request) {
        Map<String, Object> source = parameterProfile(profileId);
        Map<String, Object> version = versionRow(String.valueOf(source.get("model_version_id")));
        ModelParameterValidator.ValidationResult validation = validator.validate(
                String.valueOf(version.get("model_type")), request.parameters());
        if (!validation.valid()) {
            throw badRequest("参数校验失败: " + validation.errors().get(0).message());
        }
        Integer revision = jdbc.queryForObject("""
                select coalesce(max(revision), 0) + 1 from model_parameter_profiles
                where model_group_id=? and name=?
                """, Integer.class, source.get("model_group_id"), source.get("name"));
        String id = Ids.next("profile");
        Instant now = Instant.now();
        jdbc.update("""
                        insert into model_parameter_profiles(
                            id, model_group_id, model_version_id, name, revision, scope_type,
                            scope_value, parameters, based_on_id, status, change_reason, created_by, created_at, updated_at
                        ) values (?,?,?,?,?,?,?::jsonb,?::jsonb,?,'active',?,?,?,?)
                        """,
                id, source.get("model_group_id"), source.get("model_version_id"), source.get("name"), revision,
                source.get("scope_type"), Jsonb.write(request.scopeValue() == null ? source.get("scope_value") : request.scopeValue()),
                Jsonb.write(validation.normalizedParameters()), profileId, request.changeReason(),
                defaultString(request.operator(), "算法工程师"), Timestamp.from(now), Timestamp.from(now));
        audit("parameter_profile", id, "revision_created", request.operator(), source,
                Map.of("revision", revision, "parameters", validation.normalizedParameters()), request.changeReason());
        return parameterProfile(id);
    }

    public Map<String, Object> validateParameters(ModelManagementRequests.ValidateParametersRequest request) {
        Map<String, Object> version = versionRow(request.modelVersionId());
        requireGroupVersion(request.modelGroupId(), version);
        ModelParameterValidator.ValidationResult result = validator.validate(String.valueOf(version.get("model_type")), request.parameters());
        return Map.of(
                "valid", result.valid(),
                "normalized_parameters", result.normalizedParameters(),
                "errors", result.errors(),
                "warnings", result.warnings(),
                "resource_estimate", result.resourceEstimate()
        );
    }

    public Map<String, Object> compareParameterProfiles(List<String> profileIds) {
        if (profileIds == null || profileIds.size() < 2 || profileIds.size() > 6) {
            throw badRequest("请选择 2 至 6 个参数方案进行比较");
        }
        List<Map<String, Object>> profiles = profileIds.stream().map(this::parameterProfile).toList();
        Set<String> keys = new LinkedHashSet<>();
        profiles.forEach(profile -> keys.addAll(castMap(profile.get("parameters")).keySet()));
        List<Map<String, Object>> differences = new ArrayList<>();
        for (String key : keys) {
            List<Object> values = profiles.stream().map(profile -> castMap(profile.get("parameters")).get(key)).toList();
            boolean changed = values.stream().map(String::valueOf).distinct().count() > 1;
            differences.add(Map.of("parameter", key, "values", values, "changed", changed));
        }
        return Map.of("profiles", profiles, "differences", differences);
    }

    @Transactional
    public Map<String, Object> activateParameterProfile(String profileId, String operator) {
        Map<String, Object> before = parameterProfile(profileId);
        jdbc.update("update model_parameter_profiles set status='active', updated_at=? where id=?",
                Timestamp.from(Instant.now()), profileId);
        audit("parameter_profile", profileId, "activated", operator, before, Map.of("status", "active"), "启用参数方案");
        return parameterProfile(profileId);
    }

    @Transactional
    public Map<String, Object> deactivateParameterProfile(String profileId, String operator) {
        Map<String, Object> before = parameterProfile(profileId);
        if ("locked".equals(before.get("status"))) {
            throw conflict("生产快照不可停用");
        }
        jdbc.update("update model_parameter_profiles set status='inactive', updated_at=? where id=?",
                Timestamp.from(Instant.now()), profileId);
        audit("parameter_profile", profileId, "deactivated", operator, before, Map.of("status", "inactive"), "停用参数方案");
        return parameterProfile(profileId);
    }

    public List<Map<String, Object>> listModelSamples(
            String modelGroupId,
            String keyword,
            String modality,
            String labelStatus,
            String difficulty
    ) {
        List<Object> args = new ArrayList<>();
        StringBuilder where = new StringBuilder(" where s.model_group_id=? ");
        args.add(modelGroupId);
        if (keyword != null && !keyword.isBlank()) {
            where.append(" and (lower(s.name) like ? or lower(s.scene) like ? or lower(coalesce(s.line_id,'')) like ?) ");
            String value = "%" + keyword.toLowerCase(Locale.ROOT) + "%";
            args.add(value);
            args.add(value);
            args.add(value);
        }
        if (modality != null && !modality.isBlank()) {
            where.append(" and s.modality=? ");
            args.add(modality);
        }
        if (labelStatus != null && !labelStatus.isBlank()) {
            where.append(" and s.label_status=? ");
            args.add(labelStatus);
        }
        if (difficulty != null && !difficulty.isBlank()) {
            where.append(" and s.difficulty_tags @> ?::jsonb ");
            args.add(Jsonb.write(List.of(difficulty)));
        }
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select s.id, s.name, s.model_group_id, s.modality, s.storage_url, s.thumbnail_url,
                       s.line_id, s.mileage, s.scene, s.source, s.label_status, s.split_type,
                       s.quality_status, s.difficulty_tags::text as difficulty_tags,
                       s.metadata::text as metadata, s.ground_truth::text as ground_truth,
                       s.sha256, s.created_at
                from model_samples s
                """ + where + " order by s.created_at desc, s.id", args.toArray());
        return parseRows(rows, "difficulty_tags", "metadata", "ground_truth");
    }

    public List<Map<String, Object>> listSampleSets(String modelGroupId) {
        requireModelGroup(modelGroupId);
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select id, model_group_id, name, version, purpose, sample_ids::text as sample_ids,
                       locked, created_by, created_at
                from model_sample_sets where model_group_id=? order by created_at desc
                """, modelGroupId);
        return parseRows(rows, "sample_ids");
    }

    public Map<String, Object> previewSampleSet(List<String> sampleIds) {
        if (sampleIds == null || sampleIds.isEmpty()) {
            throw badRequest("请选择样本");
        }
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select id, modality, scene, label_status, quality_status,
                       coalesce((metadata->>'size_bytes')::bigint, 0) as size_bytes
                from model_samples where id in (%s)
                """.formatted(String.join(",", sampleIds.stream().map(id -> "?").toList())), sampleIds.toArray());
        Map<String, Long> modalities = rows.stream().collect(java.util.stream.Collectors.groupingBy(
                row -> String.valueOf(row.get("modality")), LinkedHashMap::new, java.util.stream.Collectors.counting()));
        Map<String, Long> scenes = rows.stream().collect(java.util.stream.Collectors.groupingBy(
                row -> String.valueOf(row.get("scene")), LinkedHashMap::new, java.util.stream.Collectors.counting()));
        long labeled = rows.stream().filter(row -> "reviewed".equals(String.valueOf(row.get("label_status")))).count();
        long bytes = rows.stream().mapToLong(row -> ((Number) row.get("size_bytes")).longValue()).sum();
        return Map.of("sample_count", rows.size(), "total_size_bytes", bytes, "modalities", modalities,
                "scenes", scenes, "ground_truth_coverage", rows.isEmpty() ? 0 : Math.round(labeled * 100.0 / rows.size()));
    }

    @Transactional
    public Map<String, Object> createUploadSession(ModelManagementRequests.CreateUploadSessionRequest request) {
        requireModelGroup(request.modelGroupId());
        int retentionDays = Math.max(1, Math.min(request.retentionDays() == null ? 7 : request.retentionDays(), 30));
        String id = Ids.next("upload");
        Instant now = Instant.now();
        String objectPrefix = "temporary/" + now.toString().substring(0, 10) + "/" + id;
        jdbc.update("""
                        insert into test_upload_sessions(
                            id, model_group_id, status, object_prefix, created_by, expires_at, cleanup_status, created_at
                        ) values (?,?,'uploading',?,?,?,'pending',?)
                        """,
                id, request.modelGroupId(), objectPrefix, defaultString(request.operator(), "测试人员"),
                Timestamp.from(now.plus(retentionDays, ChronoUnit.DAYS)), Timestamp.from(now));
        return uploadSession(id);
    }

    @Transactional
    public Map<String, Object> createChunkedFile(String sessionId, ModelManagementRequests.CreateChunkedFileRequest request) {
        Map<String, Object> session = uploadSession(sessionId);
        if (!"uploading".equals(String.valueOf(session.get("status")))) {
            throw conflict("上传会话当前状态不可创建分片文件");
        }
        long totalSize = request.totalSizeBytes() == null ? 0 : request.totalSizeBytes();
        if (totalSize <= 0 || totalSize > 2L * 1024 * 1024 * 1024) {
            throw badRequest("分片文件大小必须在 1 字节到 2 GB 之间");
        }
        int partSize = Math.max(1024 * 1024, Math.min(request.partSizeBytes() == null ? 8 * 1024 * 1024 : request.partSizeBytes(), 16 * 1024 * 1024));
        int totalParts = (int) Math.ceil((double) totalSize / partSize);
        String expectedSha = request.sha256() == null ? null : request.sha256().trim().toLowerCase(Locale.ROOT);
        if (expectedSha != null && !expectedSha.isBlank() && !expectedSha.matches("[a-f0-9]{64}")) {
            throw badRequest("SHA-256 格式不正确");
        }
        String manifestId = Ids.next("upload-manifest");
        jdbc.update("""
                insert into test_upload_manifests(
                    id, session_id, file_name, content_type, total_size_bytes, part_size_bytes,
                    total_parts, expected_sha256, status, created_at
                ) values (?,?,?,?,?,?,?,?,'uploading',?)
                """, manifestId, sessionId, request.fileName(), defaultString(request.contentType(), "application/octet-stream"),
                totalSize, partSize, totalParts, expectedSha, Timestamp.from(Instant.now()));
        return chunkedManifest(manifestId);
    }

    @Transactional
    public Map<String, Object> uploadChunk(String sessionId, String manifestId, int partIndex, MultipartFile chunk) {
        Map<String, Object> manifest = chunkedManifest(manifestId);
        if (!Objects.equals(manifest.get("session_id"), sessionId)) {
            throw badRequest("分片文件不属于当前上传会话");
        }
        if (!"uploading".equals(String.valueOf(manifest.get("status")))) {
            throw conflict("分片文件已经完成或不可继续上传");
        }
        int totalParts = ((Number) manifest.get("total_parts")).intValue();
        int partSize = ((Number) manifest.get("part_size_bytes")).intValue();
        if (partIndex < 0 || partIndex >= totalParts) {
            throw badRequest("分片序号超出范围");
        }
        if (chunk == null || chunk.isEmpty() || chunk.getSize() > partSize) {
            throw badRequest("分片内容为空或超过计划大小");
        }
        ModelTestStorageService.PartObject stored = storage.storePart(sessionId, manifestId, partIndex, chunk);
        jdbc.update("""
                insert into test_upload_parts(id, manifest_id, part_index, size_bytes, sha256, created_at)
                values (?,?,?,?,?,?)
                on conflict(manifest_id, part_index) do update
                  set size_bytes=excluded.size_bytes, sha256=excluded.sha256, created_at=excluded.created_at
                """, Ids.next("upload-part"), manifestId, partIndex, stored.sizeBytes(), stored.sha256(), Timestamp.from(Instant.now()));
        return chunkedManifest(manifestId);
    }

    @Transactional
    public Map<String, Object> completeChunkedFile(String sessionId, String manifestId) {
        Map<String, Object> manifest = chunkedManifest(manifestId);
        if (!Objects.equals(manifest.get("session_id"), sessionId)) {
            throw badRequest("分片文件不属于当前上传会话");
        }
        if ("completed".equals(String.valueOf(manifest.get("status")))) {
            return uploadFile(String.valueOf(manifest.get("upload_file_id")));
        }
        int totalParts = ((Number) manifest.get("total_parts")).intValue();
        long uploadedParts = ((Number) manifest.get("uploaded_part_count")).longValue();
        long uploadedBytes = ((Number) manifest.get("uploaded_size_bytes")).longValue();
        long totalSize = ((Number) manifest.get("total_size_bytes")).longValue();
        if (uploadedParts != totalParts || uploadedBytes != totalSize) {
            throw badRequest("分片尚未完整上传");
        }
        Map<String, Object> session = uploadSession(sessionId);
        Map<String, Object> group = requireModelGroup(String.valueOf(session.get("model_group_id")));
        String originalName = String.valueOf(manifest.get("file_name"));
        String safeName = safeFileName(originalName);
        String contentType = String.valueOf(manifest.get("content_type"));
        String modality = detectModality(safeName, contentType);
        if ("thermal".equals(group.get("model_type")) && "raster".equals(modality)) {
            modality = "thermal";
        }
        Set<String> compatible = new HashSet<>(Jsonb.stringList(String.valueOf(group.get("input_modalities_text"))));
        List<Map<String, Object>> messages = new ArrayList<>();
        String validationStatus = "valid";
        if (!compatible.contains(modality)) {
            validationStatus = "invalid";
            messages.add(Map.of("level", "error", "code", "incompatible_modality", "message", "文件模态与当前模型不兼容"));
        }
        String fileId = Ids.next("upload-file");
        String objectKey = session.get("object_prefix") + "/" + fileId + "-" + safeName;
        ModelTestStorageService.StoredObject object = storage.completeParts(objectKey, sessionId, manifestId, totalParts,
                contentType, defaultString(manifest.get("expected_sha256"), ""));
        Map<String, Object> metadata = uploadMetadata(modality, originalName, object.localFallback(), String.valueOf(group.get("model_type")));
        jdbc.update("""
                insert into test_upload_files(
                    id, session_id, file_name, content_type, modality, size_bytes, sha256,
                    object_key, storage_url, metadata, validation_status, validation_messages, created_at
                ) values (?,?,?,?,?,?,?,?,?,?::jsonb,?,?::jsonb,?)
                """, fileId, sessionId, originalName, contentType, modality, object.sizeBytes(), object.sha256(),
                object.objectKey(), object.storageUrl(), Jsonb.write(metadata), validationStatus, Jsonb.write(messages), Timestamp.from(Instant.now()));
        jdbc.update("update test_upload_manifests set status='completed', upload_file_id=?, completed_at=? where id=?",
                fileId, Timestamp.from(Instant.now()), manifestId);
        jdbc.update("update test_upload_sessions set file_count=file_count+1, total_size_bytes=total_size_bytes+? where id=?",
                object.sizeBytes(), sessionId);
        return uploadFile(fileId);
    }

    @Transactional
    public Map<String, Object> uploadFiles(String sessionId, MultipartFile[] files) {
        Map<String, Object> session = uploadSession(sessionId);
        if (!Set.of("uploading", "completed").contains(String.valueOf(session.get("status")))) {
            throw conflict("上传会话当前状态不可接收文件");
        }
        if (files == null || files.length == 0) {
            throw badRequest("请选择需要上传的文件");
        }
        int existing = ((Number) session.get("file_count")).intValue();
        if (existing + files.length > MAX_UPLOAD_FILES) {
            throw badRequest("单个上传会话最多允许 " + MAX_UPLOAD_FILES + " 个文件");
        }
        Map<String, Object> group = requireModelGroup(String.valueOf(session.get("model_group_id")));
        Set<String> compatibleModalities = new HashSet<>(Jsonb.stringList(String.valueOf(group.get("input_modalities_text"))));
        List<Map<String, Object>> stored = new ArrayList<>();
        long addedBytes = 0;
        for (MultipartFile file : files) {
            try {
                if (file.isEmpty()) {
                    continue;
                }
                String originalName = defaultString(file.getOriginalFilename(), "uploaded-file");
                String safeName = safeFileName(originalName);
                String modality = detectModality(safeName, file.getContentType());
                if ("thermal".equals(group.get("model_type")) && "raster".equals(modality)) {
                    modality = "thermal";
                }
                List<Map<String, Object>> messages = new ArrayList<>();
                String validationStatus = "valid";
                if (!compatibleModalities.contains(modality)) {
                    validationStatus = "invalid";
                    messages.add(Map.of("level", "error", "code", "incompatible_modality", "message", "文件模态与当前模型不兼容"));
                }
                if (file.getSize() > 2L * 1024 * 1024 * 1024) {
                    throw badRequest("单文件大小不能超过 2 GB");
                }
                String fileId = Ids.next("upload-file");
                String objectKey = session.get("object_prefix") + "/" + fileId + "-" + safeName;
                ModelTestStorageService.StoredObject object = storage.store(objectKey, file);
                Map<String, Object> metadata = uploadMetadata(modality, originalName, object.localFallback(), String.valueOf(group.get("model_type")));
                jdbc.update("""
                                insert into test_upload_files(
                                    id, session_id, file_name, content_type, modality, size_bytes, sha256,
                                    object_key, storage_url, metadata, validation_status, validation_messages, created_at
                                ) values (?,?,?,?,?,?,?,?,?,?::jsonb,?,?::jsonb,?)
                                """,
                        fileId, sessionId, originalName, defaultString(file.getContentType(), "application/octet-stream"),
                        modality, object.sizeBytes(), object.sha256(), object.objectKey(), object.storageUrl(),
                        Jsonb.write(metadata), validationStatus, Jsonb.write(messages), Timestamp.from(Instant.now()));
                addedBytes += object.sizeBytes();
                stored.add(uploadFile(fileId));
            } catch (ResponseStatusException e) {
                throw e;
            } catch (Exception e) {
                throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "上传文件保存失败: " + e.getMessage(), e);
            }
        }
        jdbc.update("""
                update test_upload_sessions set file_count=file_count+?, total_size_bytes=total_size_bytes+?, status='uploading'
                where id=?
                """, stored.size(), addedBytes, sessionId);
        return Map.of("session", uploadSession(sessionId), "files", stored);
    }

    @Transactional
    public Map<String, Object> completeUploadSession(String sessionId) {
        Map<String, Object> session = uploadSession(sessionId);
        if (((Number) session.get("file_count")).intValue() == 0) {
            throw badRequest("上传会话中没有可用文件");
        }
        Long invalid = jdbc.queryForObject("select count(*) from test_upload_files where session_id=? and validation_status='invalid'", Long.class, sessionId);
        String status = invalid != null && invalid > 0 ? "completed_with_errors" : "completed";
        jdbc.update("update test_upload_sessions set status=?, completed_at=? where id=?",
                status, Timestamp.from(Instant.now()), sessionId);
        return uploadSession(sessionId);
    }

    public Map<String, Object> uploadSession(String sessionId) {
        Map<String, Object> row;
        try {
            row = jdbc.queryForMap("""
                    select id, model_group_id, status, file_count, total_size_bytes, object_prefix,
                           created_by, expires_at, completed_at, cleanup_status, created_at
                    from test_upload_sessions where id=?
                    """, sessionId);
        } catch (Exception e) {
            throw notFound("上传会话不存在");
        }
        row = new HashMap<>(row);
        row.put("files", uploadFiles(sessionId));
        row.put("chunked_files", jdbc.queryForList("select id from test_upload_manifests where session_id=? order by created_at", sessionId)
                .stream().map(item -> chunkedManifest(String.valueOf(item.get("id")))).toList());
        row.put("storage_mode", storage.isMinioAvailable() ? "object_storage" : "local_fallback");
        return row;
    }

    @Transactional
    public Map<String, Object> deleteUploadSession(String sessionId) {
        Map<String, Object> session = uploadSession(sessionId);
        Long references = jdbc.queryForObject("select count(*) from model_test_inputs where upload_file_id in (select id from test_upload_files where session_id=?)", Long.class, sessionId);
        if (references != null && references > 0) {
            throw conflict("上传文件已被测试任务引用，不能删除");
        }
        List<Map<String, Object>> files = uploadFiles(sessionId);
        for (Map<String, Object> file : files) {
            storage.delete(String.valueOf(file.get("object_key")), String.valueOf(file.get("storage_url")));
        }
        jdbc.queryForList("select id, total_parts from test_upload_manifests where session_id=?", sessionId).forEach(manifest ->
                storage.cleanupParts(sessionId, String.valueOf(manifest.get("id")), ((Number) manifest.get("total_parts")).intValue()));
        audit("test_upload_session", sessionId, "cleaned", "对象生命周期任务", session,
                Map.of("cleanup_status", "completed", "file_count", files.size()), "删除临时上传及对象");
        jdbc.update("delete from test_upload_sessions where id=?", sessionId);
        return Map.of("session_id", sessionId, "deleted", true, "file_count", session.get("file_count"));
    }

    @Transactional
    public int cleanupExpiredUploadSessions() {
        List<String> sessionIds = jdbc.queryForList("""
                select s.id from test_upload_sessions s
                where s.expires_at < now() and s.cleanup_status in ('pending', 'failed')
                  and not exists (
                    select 1 from model_test_inputs i
                    join test_upload_files f on f.id=i.upload_file_id
                    where f.session_id=s.id
                  )
                order by s.expires_at asc limit 100
                """, String.class);
        int cleaned = 0;
        for (String sessionId : sessionIds) {
            try {
                deleteUploadSession(sessionId);
                cleaned++;
            } catch (Exception e) {
                jdbc.update("update test_upload_sessions set cleanup_status='failed' where id=?", sessionId);
            }
        }
        return cleaned;
    }

    public UploadedContent uploadedContent(String fileId) {
        Map<String, Object> file = uploadFile(fileId);
        byte[] content = storage.read(String.valueOf(file.get("object_key")), String.valueOf(file.get("storage_url")));
        return new UploadedContent(content, String.valueOf(file.get("content_type")), String.valueOf(file.get("file_name")));
    }

    @Transactional
    public Map<String, Object> createTest(ModelManagementRequests.CreateModelTestRequest request) {
        Map<String, Object> group = requireModelGroup(request.modelGroupId());
        if (request.matrix().size() > MAX_TEST_MATRIX) {
            throw badRequest("测试矩阵最多允许 " + MAX_TEST_MATRIX + " 个组合");
        }
        List<Map<String, Object>> matrix = new ArrayList<>();
        for (ModelManagementRequests.TestMatrixItem item : request.matrix()) {
            Map<String, Object> version = versionRow(item.modelVersionId());
            requireGroupVersion(request.modelGroupId(), version);
            if (item.parameterProfileId() != null && !item.parameterProfileId().isBlank()) {
                Map<String, Object> profile = parameterProfile(item.parameterProfileId());
                if (!Objects.equals(profile.get("model_version_id"), item.modelVersionId())) {
                    throw badRequest("参数方案与模型版本不匹配");
                }
            }
            matrix.add(Map.of(
                    "model_version_id", item.modelVersionId(),
                    "parameter_profile_id", defaultString(item.parameterProfileId(), ""),
                    "temporary_parameters", defaultMap(item.temporaryParameters())
            ));
        }
        List<String> sampleIds = safeList(request.inputs().librarySampleIds());
        List<String> uploadSessionIds = safeList(request.inputs().uploadSessionIds());
        if (sampleIds.size() + uploadSessionIds.size() == 0) {
            throw badRequest("至少选择一个样本或上传会话");
        }
        String id = Ids.next("model-test");
        String taskNo = "MT-" + Instant.now().toEpochMilli() + "-" + id.substring(id.length() - 4).toUpperCase(Locale.ROOT);
        String traceId = Ids.next("trace");
        String ruleVersion = jdbc.queryForObject("""
                select version from model_acceptance_rules where model_group_id=? and active=true order by created_at desc limit 1
                """, String.class, request.modelGroupId());
        Map<String, Object> inputSnapshot = Map.of("library_sample_ids", sampleIds, "upload_session_ids", uploadSessionIds);
        Instant now = Instant.now();
        jdbc.update("""
                        insert into model_test_tasks(
                            id, task_no, name, test_type, model_group_id, status, current_step,
                            input_snapshot, matrix_snapshot, options, acceptance_rule_version,
                            progress, trace_id, created_by, created_at
                        ) values (?,?,?,?,?,'draft','draft',?::jsonb,?::jsonb,?::jsonb,?,0,?,?,?)
                        """,
                id, taskNo, request.name(), request.testType(), request.modelGroupId(),
                Jsonb.write(inputSnapshot), Jsonb.write(matrix), Jsonb.write(defaultMap(request.options())),
                ruleVersion, traceId, defaultString(request.operator(), "测试人员"), Timestamp.from(now));

        Set<String> hashes = new HashSet<>();
        for (String sampleId : sampleIds) {
            List<Map<String, Object>> rows = jdbc.queryForList("""
                    select id, name, storage_url, sha256, modality, scene,
                           metadata::text as metadata, ground_truth::text as ground_truth, quality_status
                    from model_samples where id=? and model_group_id=?
                    """, sampleId, request.modelGroupId());
            if (rows.isEmpty()) {
                throw badRequest("样本不存在或与当前模型不兼容: " + sampleId);
            }
            Map<String, Object> sample = rows.get(0);
            if (!hashes.add(String.valueOf(sample.get("sha256")))) {
                continue;
            }
            Map<String, Object> metadata = Jsonb.map(String.valueOf(sample.get("metadata")));
            metadata = new LinkedHashMap<>(metadata);
            metadata.put("scene", sample.get("scene"));
            insertTestInput(id, "library", String.valueOf(sample.get("id")), null,
                    String.valueOf(sample.get("name")), String.valueOf(sample.get("storage_url")), String.valueOf(sample.get("sha256")),
                    String.valueOf(sample.get("modality")), metadata, Jsonb.map(String.valueOf(sample.get("ground_truth"))),
                    "qualified".equals(sample.get("quality_status")) ? "valid" : "warning", List.of());
        }
        for (String uploadSessionId : uploadSessionIds) {
            Map<String, Object> uploadSession = uploadSession(uploadSessionId);
            if (!Objects.equals(uploadSession.get("model_group_id"), request.modelGroupId())) {
                throw badRequest("上传会话与当前模型不匹配");
            }
            for (Map<String, Object> file : uploadFiles(uploadSessionId)) {
                if (!hashes.add(String.valueOf(file.get("sha256")))) {
                    continue;
                }
                insertTestInput(id, "upload", null, String.valueOf(file.get("id")),
                        String.valueOf(file.get("file_name")), String.valueOf(file.get("storage_url")), String.valueOf(file.get("sha256")),
                        String.valueOf(file.get("modality")), castMap(file.get("metadata")), Map.of(),
                        String.valueOf(file.get("validation_status")), castList(file.get("validation_messages")));
            }
        }
        Integer inputCount = jdbc.queryForObject("select count(*) from model_test_inputs where test_task_id=?", Integer.class, id);
        if (inputCount == null || inputCount == 0) {
            throw badRequest("没有可加入测试任务的输入");
        }
        if (inputCount > MAX_TEST_INPUTS) {
            throw badRequest("单个测试任务最多允许 " + MAX_TEST_INPUTS + " 个输入");
        }
        events.publish(id, "test.created", "draft", "draft", 0, "测试任务草稿已创建", Map.of(
                "input_count", inputCount, "matrix_count", matrix.size(), "trace_id", traceId,
                "model_group", group.get("code")
        ));
        audit("model_test", id, "created", request.operator(), Map.of(), Map.of(
                "task_no", taskNo, "input_count", inputCount, "matrix_count", matrix.size()), "创建模型测试任务");
        return testDetail(id);
    }

    public Map<String, Object> validateTest(String taskId) {
        Map<String, Object> task = testDetail(taskId);
        Map<String, Object> group = requireModelGroup(String.valueOf(task.get("model_group_id")));
        Set<String> modalities = new HashSet<>(Jsonb.stringList(String.valueOf(group.get("input_modalities_text"))));
        List<Map<String, Object>> errors = new ArrayList<>();
        List<Map<String, Object>> warnings = new ArrayList<>();
        List<Map<String, Object>> inputs = castMapList(task.get("inputs"));
        for (Map<String, Object> input : inputs) {
            if (!modalities.contains(String.valueOf(input.get("modality")))) {
                errors.add(Map.of("input_id", input.get("id"), "code", "incompatible_modality", "message", "输入模态与模型不兼容"));
            }
            if ("invalid".equals(input.get("validation_status"))) {
                errors.add(Map.of("input_id", input.get("id"), "code", "invalid_input", "message", "输入文件校验未通过"));
            }
            if (castMap(input.get("ground_truth_snapshot")).isEmpty()) {
                warnings.add(Map.of("input_id", input.get("id"), "code", "missing_ground_truth", "message", "缺少真值，只计算非监督指标"));
            }
        }
        List<Map<String, Object>> matrix = castMapList(task.get("matrix_snapshot"));
        List<Map<String, Object>> parameterChecks = new ArrayList<>();
        for (Map<String, Object> combination : matrix) {
            String versionId = String.valueOf(combination.get("model_version_id"));
            Map<String, Object> version = versionRow(versionId);
            String profileId = defaultString(combination.get("parameter_profile_id"), "");
            Map<String, Object> parameters = new LinkedHashMap<>();
            if (!profileId.isBlank()) {
                parameters.putAll(castMap(parameterProfile(profileId).get("parameters")));
            }
            parameters.putAll(castMap(combination.get("temporary_parameters")));
            ModelParameterValidator.ValidationResult validation = validator.validate(String.valueOf(version.get("model_type")), parameters);
            parameterChecks.add(Map.of(
                    "model_version_id", versionId,
                    "parameter_profile_id", profileId,
                    "valid", validation.valid(),
                    "errors", validation.errors(),
                    "warnings", validation.warnings(),
                    "resource_estimate", validation.resourceEstimate()
            ));
            validation.errors().forEach(error -> errors.add(Map.of(
                    "model_version_id", versionId, "field", error.field(), "code", error.code(), "message", error.message()
            )));
            validation.warnings().forEach(warning -> warnings.add(Map.of(
                    "model_version_id", versionId, "field", warning.field(), "code", warning.code(), "message", warning.message()
            )));
        }
        int inferenceCount = inputs.size() * matrix.size();
        if (inferenceCount > MAX_TEST_INPUTS * MAX_TEST_MATRIX) {
            errors.add(Map.of("code", "matrix_limit", "message", "测试矩阵超过平台运行上限"));
        }
        return Map.of(
                "valid", errors.isEmpty(),
                "errors", errors,
                "warnings", warnings,
                "parameter_checks", parameterChecks,
                "budget", Map.of(
                        "input_count", inputs.size(), "matrix_count", matrix.size(), "inference_count", inferenceCount,
                        "estimated_minutes", Math.max(1, (int) Math.ceil(inferenceCount * estimatedSeconds(group) / 60.0)),
                        "queue", "model-test", "production_isolated", true
                )
        );
    }

    public Map<String, Object> startTest(String taskId) {
        Map<String, Object> task = testDetail(taskId);
        if (!Set.of("draft", "rejected").contains(String.valueOf(task.get("status")))) {
            throw conflict("测试任务当前状态不可启动");
        }
        jdbc.update("update model_test_tasks set status='validating', current_step='validation', progress=1 where id=?", taskId);
        events.publish(taskId, "test.validating", "validation", "running", 1, "正在校验输入、参数和资源预算", Map.of());
        Map<String, Object> validation = validateTest(taskId);
        if (!Boolean.TRUE.equals(validation.get("valid"))) {
            jdbc.update("update model_test_tasks set status='rejected', current_step='validation', error_message=? where id=?",
                    "测试校验未通过", taskId);
            events.publish(taskId, "test.rejected", "validation", "failed", 1, "测试校验未通过", validation);
            return Map.of("task", testDetail(taskId), "validation", validation);
        }
        jdbc.update("update model_test_tasks set status='queued', current_step='queue', progress=2, error_message=null where id=?", taskId);
        events.publish(taskId, "test.queued", "queue", "queued", 2, "测试任务已进入隔离队列", Map.of("queue", "model-test"));
        runner.runAsync(taskId);
        return Map.of("task", testDetail(taskId), "validation", validation);
    }

    @Transactional
    public Map<String, Object> cancelTest(String taskId) {
        Map<String, Object> task = testDetail(taskId);
        String status = String.valueOf(task.get("status"));
        if (Set.of("completed", "partially_completed", "failed", "cancelled").contains(status)) {
            throw conflict("测试任务已经结束");
        }
        if (Set.of("draft", "rejected", "queued", "validating").contains(status)) {
            jdbc.update("update model_test_tasks set cancel_requested=true, status='cancelled', current_step='cancelled', finished_at=? where id=?",
                    Timestamp.from(Instant.now()), taskId);
            events.publish(taskId, "test.cancelled", "cancelled", "cancelled", number(task.get("progress")), "测试任务已取消", Map.of());
            events.complete(taskId);
        } else {
            jdbc.update("update model_test_tasks set cancel_requested=true, status='cancelling' where id=?", taskId);
            events.publish(taskId, "test.cancelling", "cancelling", "running", number(task.get("progress")), "正在安全停止测试任务", Map.of());
        }
        return testDetail(taskId);
    }

    @Transactional
    public Map<String, Object> cloneTest(String taskId, String operator) {
        Map<String, Object> source = testDetail(taskId);
        String id = Ids.next("model-test");
        String taskNo = "MT-" + Instant.now().toEpochMilli() + "-" + id.substring(id.length() - 4).toUpperCase(Locale.ROOT);
        Instant now = Instant.now();
        jdbc.update("""
                        insert into model_test_tasks(
                            id, task_no, name, test_type, model_group_id, status, current_step,
                            input_snapshot, matrix_snapshot, options, acceptance_rule_version,
                            progress, trace_id, created_by, created_at
                        ) values (?,?,?,?,?,'draft','draft',?::jsonb,?::jsonb,?::jsonb,?,0,?,?,?)
                        """,
                id, taskNo, source.get("name") + " 副本", source.get("test_type"), source.get("model_group_id"),
                Jsonb.write(source.get("input_snapshot")), Jsonb.write(source.get("matrix_snapshot")), Jsonb.write(source.get("options")),
                source.get("acceptance_rule_version"), Ids.next("trace"), defaultString(operator, "测试人员"), Timestamp.from(now));
        jdbc.update("""
                insert into model_test_inputs(
                    id, test_task_id, source_type, sample_id, upload_file_id, name, storage_url, sha256,
                    modality, metadata_snapshot, ground_truth_snapshot, validation_status, validation_messages, created_at
                )
                select 'mti-' || substr(md5(random()::text || id), 1, 12), ?, source_type, sample_id, upload_file_id,
                       name, storage_url, sha256, modality, metadata_snapshot, ground_truth_snapshot,
                       validation_status, validation_messages, ?
                from model_test_inputs where test_task_id=?
                """, id, Timestamp.from(now), taskId);
        events.publish(id, "test.created", "draft", "draft", 0, "已从历史任务复制测试草稿", Map.of("source_task_id", taskId));
        audit("model_test", id, "cloned", operator, Map.of("source_task_id", taskId), Map.of("task_no", taskNo), "复制模型测试任务");
        return testDetail(id);
    }

    public List<Map<String, Object>> listTests(String modelGroupId, String status, int limit) {
        List<Object> args = new ArrayList<>();
        StringBuilder where = new StringBuilder(" where 1=1 ");
        if (modelGroupId != null && !modelGroupId.isBlank()) {
            where.append(" and t.model_group_id=? ");
            args.add(modelGroupId);
        }
        if (status != null && !status.isBlank()) {
            where.append(" and t.status=? ");
            args.add(status);
        }
        args.add(Math.max(1, Math.min(limit, 500)));
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select t.id, t.task_no, t.name, t.test_type, t.model_group_id, mg.name as model_group_name,
                       t.status, t.current_step, t.progress, t.trace_id, t.error_message, t.created_by,
                       t.created_at, t.started_at, t.finished_at,
                       jsonb_array_length(t.matrix_snapshot) as matrix_count,
                       (select count(*) from model_test_inputs i where i.test_task_id=t.id) as input_count,
                       (select count(*) from model_test_result_items r where r.test_task_id=t.id and r.status='success') as success_count,
                       (select count(*) from model_test_result_items r where r.test_task_id=t.id and r.status='failed') as failure_count,
                       (select string_agg(distinct v.version, ', ' order by v.version)
                        from model_test_result_items r join model_versions v on v.id=r.model_version_id where r.test_task_id=t.id) as executed_versions
                from model_test_tasks t
                join model_groups mg on mg.id=t.model_group_id
                """ + where + " order by t.created_at desc limit ?", args.toArray());
        return rows;
    }

    public Map<String, Object> testDetail(String taskId) {
        Map<String, Object> task;
        try {
            task = parseRow(jdbc.queryForMap("""
                    select t.id, t.task_no, t.name, t.test_type, t.model_group_id, mg.name as model_group_name,
                           mg.code as model_group_code, mg.model_type, t.status, t.current_step,
                           t.input_snapshot::text as input_snapshot, t.matrix_snapshot::text as matrix_snapshot,
                           t.options::text as options, t.acceptance_rule_version, t.progress, t.trace_id,
                           t.cancel_requested, t.error_message, t.created_by, t.created_at, t.started_at, t.finished_at
                    from model_test_tasks t join model_groups mg on mg.id=t.model_group_id where t.id=?
                    """, taskId), "input_snapshot", "matrix_snapshot", "options");
        } catch (Exception e) {
            throw notFound("测试任务不存在");
        }
        List<Map<String, Object>> inputs = jdbc.queryForList("""
                select id, source_type, sample_id, upload_file_id, name, storage_url, sha256, modality,
                       metadata_snapshot::text as metadata_snapshot, ground_truth_snapshot::text as ground_truth_snapshot,
                       validation_status, validation_messages::text as validation_messages, created_at
                from model_test_inputs where test_task_id=? order by created_at, id
                """, taskId);
        task.put("inputs", parseRows(inputs, "metadata_snapshot", "ground_truth_snapshot", "validation_messages"));
        task.put("events", events.events(taskId, 0));
        task.put("summaries", testSummaries(taskId));
        return task;
    }

    public List<Map<String, Object>> testResults(String taskId) {
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select r.id, r.test_task_id, r.test_input_id, i.name as input_name, i.modality,
                       i.source_type, i.upload_file_id, i.storage_url, i.metadata_snapshot::text as input_metadata,
                       r.model_version_id, v.version as model_version, v.display_name as model_version_name,
                       r.parameter_profile_id, p.name as parameter_profile_name, p.revision as parameter_revision,
                       r.parameter_snapshot::text as parameter_snapshot, r.status,
                       r.prediction::text as prediction, r.metrics::text as metrics,
                       r.artifacts::text as artifacts, r.runtime_snapshot::text as runtime_snapshot,
                       r.timings::text as timings, r.error_code, r.error_detail::text as error_detail,
                       r.created_at, r.completed_at
                from model_test_result_items r
                join model_test_inputs i on i.id=r.test_input_id
                join model_versions v on v.id=r.model_version_id
                left join model_parameter_profiles p on p.id=r.parameter_profile_id
                where r.test_task_id=? order by i.created_at, v.version, r.created_at
                """, taskId);
        List<Map<String, Object>> parsed = parseRows(rows, "input_metadata", "parameter_snapshot", "prediction", "metrics", "artifacts", "runtime_snapshot", "timings", "error_detail");
        for (Map<String, Object> row : parsed) {
            if (row.get("upload_file_id") != null) {
                row.put("preview_url", "/api/v1/test-upload-files/" + row.get("upload_file_id") + "/content");
            }
        }
        return parsed;
    }

    public List<Map<String, Object>> testSummaries(String taskId) {
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select s.id, s.test_task_id, s.model_version_id, v.version as model_version,
                       v.display_name as model_version_name, s.parameter_profile_id,
                       p.name as parameter_profile_name, p.revision as parameter_revision,
                       s.metrics::text as metrics, s.class_metrics::text as class_metrics,
                       s.baseline_delta::text as baseline_delta, s.acceptance_status,
                       s.acceptance_details::text as acceptance_details, s.conclusion,
                       s.report_object_key, s.metric_calculator_version, s.created_at
                from model_test_summaries s
                join model_versions v on v.id=s.model_version_id
                left join model_parameter_profiles p on p.id=s.parameter_profile_id
                where s.test_task_id=? order by v.version, p.revision
                """, taskId);
        return parseRows(rows, "metrics", "class_metrics", "baseline_delta", "acceptance_details");
    }

    public TestReport testReport(String taskId) {
        Map<String, Object> task = testDetail(taskId);
        List<Map<String, Object>> summaries = testSummaries(taskId);
        List<Map<String, Object>> results = testResults(taskId);
        StringBuilder report = new StringBuilder();
        report.append("# 模型测试评估报告\n\n")
                .append("- 任务编号：").append(markdown(task.get("task_no"))).append("\n")
                .append("- 任务名称：").append(markdown(task.get("name"))).append("\n")
                .append("- 模型组：").append(markdown(task.get("model_group_name"))).append("\n")
                .append("- 测试类型：").append(markdown(task.get("test_type"))).append("\n")
                .append("- 状态：").append(markdown(task.get("status"))).append("\n")
                .append("- 追踪号：").append(markdown(task.get("trace_id"))).append("\n")
                .append("- 创建时间：").append(markdown(task.get("created_at"))).append("\n\n")
                .append("## 验收结论\n\n");
        for (Map<String, Object> summary : summaries) {
            report.append("### ").append(markdown(summary.get("model_version"))).append(" / ")
                    .append(markdown(summary.get("parameter_profile_name"))).append("\n\n")
                    .append("结论：").append(markdown(summary.get("acceptance_status"))).append("，")
                    .append(markdown(summary.get("conclusion"))).append("\n\n")
                    .append("| 指标 | 数值 |\n| --- | ---: |\n");
            castMap(summary.get("metrics")).forEach((key, value) ->
                    report.append("| ").append(markdown(key)).append(" | ").append(markdown(value)).append(" |\n"));
            report.append("\n");
        }
        report.append("## 结果明细\n\n| 输入 | 模态 | 版本 | 状态 | 总耗时 |\n| --- | --- | --- | --- | ---: |\n");
        for (Map<String, Object> result : results) {
            Map<String, Object> timings = castMap(result.get("timings"));
            report.append("| ").append(markdown(result.get("input_name"))).append(" | ")
                    .append(markdown(result.get("modality"))).append(" | ")
                    .append(markdown(result.get("model_version"))).append(" | ")
                    .append(markdown(result.get("status"))).append(" | ")
                    .append(markdown(timings.getOrDefault("total_ms", 0))).append(" ms |\n");
        }
        report.append("\n## 可复现快照\n\n```json\n")
                .append(Jsonb.write(Map.of(
                        "task_id", taskId,
                        "trace_id", task.get("trace_id"),
                        "input_snapshot", task.get("input_snapshot"),
                        "matrix_snapshot", task.get("matrix_snapshot"),
                        "options", task.get("options")
                )))
                .append("\n```\n");
        String taskNo = String.valueOf(task.get("task_no")).replaceAll("[^A-Za-z0-9_-]", "_");
        return new TestReport(taskNo + "-report.md", report.toString());
    }

    public Map<String, Object> compareTests(List<String> taskIds) {
        if (taskIds == null || taskIds.isEmpty() || taskIds.size() > 6) {
            throw badRequest("请选择 1 至 6 个测试任务进行比较");
        }
        List<Map<String, Object>> series = new ArrayList<>();
        for (String taskId : taskIds) {
            Map<String, Object> task = testDetail(taskId);
            for (Map<String, Object> summary : testSummaries(taskId)) {
                Map<String, Object> item = new LinkedHashMap<>();
                item.put("task_id", taskId);
                item.put("task_name", task.get("name"));
                item.put("model_version", summary.get("model_version"));
                item.put("parameter_profile_name", summary.get("parameter_profile_name"));
                item.put("metrics", summary.get("metrics"));
                item.put("baseline_delta", summary.get("baseline_delta"));
                item.put("acceptance_status", summary.get("acceptance_status"));
                series.add(item);
            }
        }
        return Map.of("task_ids", taskIds, "series", series);
    }

    @Transactional
    public Map<String, Object> addResultFeedback(String resultId, ModelManagementRequests.ResultFeedbackRequest request) {
        Map<String, Object> result;
        try {
            result = jdbc.queryForMap("""
                    select r.id, r.test_input_id, r.model_version_id, r.prediction::text as prediction,
                           i.name, i.storage_url, i.sha256, i.modality, i.metadata_snapshot::text as metadata,
                           t.model_group_id
                    from model_test_result_items r
                    join model_test_inputs i on i.id=r.test_input_id
                    join model_test_tasks t on t.id=r.test_task_id
                    where r.id=?
                    """, resultId);
        } catch (Exception e) {
            throw notFound("测试结果不存在");
        }
        String feedbackId = Ids.next("feedback");
        boolean archive = Boolean.TRUE.equals(request.archiveAsSample());
        jdbc.update("""
                insert into model_test_feedback(id, result_item_id, feedback_type, comment, archive_as_sample, created_by, created_at)
                values (?,?,?,?,?,?,?)
                """, feedbackId, resultId, request.feedbackType(), request.comment(), archive,
                defaultString(request.operator(), "算法数据管理员"), Timestamp.from(Instant.now()));
        String sampleId = null;
        if (archive) {
            sampleId = Ids.next("model-sample");
            Map<String, Object> metadata = Jsonb.map(String.valueOf(result.get("metadata")));
            jdbc.update("""
                            insert into model_samples(
                                id, name, model_group_id, modality, storage_url, scene, source, label_status,
                                split_type, quality_status, difficulty_tags, metadata, ground_truth, sha256, created_at
                            ) values (?,?,?,?,?,?,'测试结果回流','unlabeled','candidate','pending',?::jsonb,?::jsonb,'{}'::jsonb,?,?)
                            """,
                    sampleId, result.get("name") + " 回流", result.get("model_group_id"), result.get("modality"), result.get("storage_url"),
                    metadata.getOrDefault("scene", "待分类"), Jsonb.write(List.of(request.feedbackType())), Jsonb.write(metadata),
                    result.get("sha256"), Timestamp.from(Instant.now()));
        }
        audit("model_test_result", resultId, "feedback_created", request.operator(), Map.of(), Map.of(
                "feedback_type", request.feedbackType(), "archive_as_sample", archive), request.comment());
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("feedback_id", feedbackId);
        response.put("result_id", resultId);
        response.put("archive_as_sample", archive);
        response.put("sample_id", sampleId);
        return response;
    }

    public List<Map<String, Object>> listReleases(String modelGroupId) {
        List<Object> args = new ArrayList<>();
        String where = "";
        if (modelGroupId != null && !modelGroupId.isBlank()) {
            where = " where r.model_group_id=? ";
            args.add(modelGroupId);
        }
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select r.id, r.release_no, r.model_group_id, mg.name as model_group_name,
                       r.model_version_id, v.version as model_version, v.display_name as model_version_name,
                       r.parameter_profile_id, p.name as parameter_profile_name, p.revision as parameter_revision,
                       r.evaluation_task_id, t.task_no as evaluation_task_no, r.status,
                       r.gray_strategy::text as gray_strategy, r.gray_percent, r.applicant, r.approver,
                       r.approval_comment, r.rollback_version_id, r.rollback_profile_id,
                       r.created_at, r.approved_at, r.effective_at, r.updated_at
                from model_release_records r
                join model_groups mg on mg.id=r.model_group_id
                join model_versions v on v.id=r.model_version_id
                join model_parameter_profiles p on p.id=r.parameter_profile_id
                left join model_test_tasks t on t.id=r.evaluation_task_id
                """ + where + " order by r.created_at desc", args.toArray());
        return parseRows(rows, "gray_strategy");
    }

    @Transactional
    public Map<String, Object> createRelease(ModelManagementRequests.CreateReleaseRequest request) {
        Map<String, Object> version = versionRow(request.modelVersionId());
        requireGroupVersion(request.modelGroupId(), version);
        Map<String, Object> profile = parameterProfile(request.parameterProfileId());
        if (!Objects.equals(profile.get("model_version_id"), request.modelVersionId())) {
            throw badRequest("生产参数方案与模型版本不匹配");
        }
        if (request.evaluationTaskId() != null && !request.evaluationTaskId().isBlank()) {
            Map<String, Object> evaluation = testDetail(request.evaluationTaskId());
            if (!Set.of("completed", "partially_completed").contains(String.valueOf(evaluation.get("status")))) {
                throw badRequest("关联评估任务尚未完成");
            }
            boolean passed = testSummaries(request.evaluationTaskId()).stream()
                    .anyMatch(summary -> Objects.equals(summary.get("model_version_id"), request.modelVersionId()) && "passed".equals(summary.get("acceptance_status")));
            if (!passed) {
                throw badRequest("关联评估任务中没有该版本的通过结论");
            }
        }
        Map<String, Object> current = currentProduction(request.modelGroupId());
        String id = Ids.next("release");
        String releaseNo = "REL-" + Instant.now().toEpochMilli();
        Instant now = Instant.now();
        jdbc.update("""
                        insert into model_release_records(
                            id, release_no, model_group_id, model_version_id, parameter_profile_id,
                            evaluation_task_id, status, gray_strategy, gray_percent, applicant,
                            rollback_version_id, rollback_profile_id, created_at, updated_at
                        ) values (?,?,?,?,?,?,'requested',?::jsonb,0,?,?,?,?,?)
                        """,
                id, releaseNo, request.modelGroupId(), request.modelVersionId(), request.parameterProfileId(),
                request.evaluationTaskId(), Jsonb.write(defaultMap(request.grayStrategy())),
                defaultString(request.applicant(), "算法工程师"), current.get("model_version_id"), current.get("parameter_profile_id"),
                Timestamp.from(now), Timestamp.from(now));
        releaseEvent(id, "requested", null, "requested", request.applicant(), request.comment(), Map.of());
        audit("model_release", id, "requested", request.applicant(), Map.of(), Map.of(
                "model_version_id", request.modelVersionId(), "parameter_profile_id", request.parameterProfileId()), request.comment());
        return release(id);
    }

    @Transactional
    public Map<String, Object> approveRelease(String releaseId, ModelManagementRequests.ReleaseDecisionRequest request) {
        Map<String, Object> before = release(releaseId);
        requireReleaseStatus(before, Set.of("requested"));
        Instant now = Instant.now();
        jdbc.update("update model_release_records set status='approved', approver=?, approval_comment=?, approved_at=?, updated_at=? where id=?",
                defaultString(request.operator(), "发布审批人"), request.comment(), Timestamp.from(now), Timestamp.from(now), releaseId);
        releaseEvent(releaseId, "approved", String.valueOf(before.get("status")), "approved", request.operator(), request.comment(), Map.of());
        return release(releaseId);
    }

    @Transactional
    public Map<String, Object> rejectRelease(String releaseId, ModelManagementRequests.ReleaseDecisionRequest request) {
        Map<String, Object> before = release(releaseId);
        requireReleaseStatus(before, Set.of("requested", "approved"));
        jdbc.update("update model_release_records set status='rejected', approver=?, approval_comment=?, updated_at=? where id=?",
                defaultString(request.operator(), "发布审批人"), request.comment(), Timestamp.from(Instant.now()), releaseId);
        releaseEvent(releaseId, "rejected", String.valueOf(before.get("status")), "rejected", request.operator(), request.comment(), Map.of());
        return release(releaseId);
    }

    @Transactional
    public Map<String, Object> grayRelease(String releaseId, ModelManagementRequests.GrayReleaseRequest request) {
        Map<String, Object> before = release(releaseId);
        requireReleaseStatus(before, Set.of("approved", "gray"));
        int percent = Math.max(1, Math.min(request.grayPercent() == null ? 10 : request.grayPercent(), 99));
        jdbc.update("update model_release_records set status='gray', gray_percent=?, gray_strategy=?::jsonb, updated_at=? where id=?",
                percent, Jsonb.write(defaultMap(request.strategy())), Timestamp.from(Instant.now()), releaseId);
        releaseEvent(releaseId, "gray_changed", String.valueOf(before.get("status")), "gray", request.operator(), request.comment(), Map.of("gray_percent", percent));
        return release(releaseId);
    }

    @Transactional
    public Map<String, Object> promoteRelease(String releaseId, ModelManagementRequests.ReleaseDecisionRequest request) {
        Map<String, Object> before = release(releaseId);
        requireReleaseStatus(before, Set.of("approved", "gray"));
        Instant now = Instant.now();
        String groupId = String.valueOf(before.get("model_group_id"));
        String groupCode = modelGroupCode(groupId);
        jdbc.update("update model_versions set status='retired' where model_group=? and status='production' and id<>?",
                groupCode, before.get("model_version_id"));
        jdbc.update("update model_versions set status='production' where id=?", before.get("model_version_id"));
        jdbc.update("update model_parameter_profiles set status='locked', scope_type='production_snapshot', updated_at=? where id=?",
                Timestamp.from(now), before.get("parameter_profile_id"));
        jdbc.update("update model_release_records set status='production', gray_percent=100, effective_at=?, updated_at=? where id=?",
                Timestamp.from(now), Timestamp.from(now), releaseId);
        releaseEvent(releaseId, "promoted", String.valueOf(before.get("status")), "production", request.operator(), request.comment(), Map.of("gray_percent", 100));
        audit("model_release", releaseId, "promoted", request.operator(), before, Map.of("status", "production"), request.comment());
        return release(releaseId);
    }

    @Transactional
    public Map<String, Object> rollbackRelease(String releaseId, ModelManagementRequests.RollbackReleaseRequest request) {
        Map<String, Object> before = release(releaseId);
        Map<String, Object> targetVersion = versionRow(request.targetVersionId());
        requireGroupVersion(String.valueOf(before.get("model_group_id")), targetVersion);
        Map<String, Object> targetProfile = parameterProfile(request.targetProfileId());
        if (!Objects.equals(targetProfile.get("model_version_id"), request.targetVersionId())) {
            throw badRequest("回滚参数方案与目标模型版本不匹配");
        }
        String groupCode = modelGroupCode(String.valueOf(before.get("model_group_id")));
        Instant now = Instant.now();
        jdbc.update("update model_versions set status=case when id=? then 'candidate' else 'retired' end where model_group=? and status='production'",
                before.get("model_version_id"), groupCode);
        jdbc.update("update model_versions set status='production' where id=?", request.targetVersionId());
        jdbc.update("update model_parameter_profiles set status='locked', scope_type='production_snapshot', updated_at=? where id=?",
                Timestamp.from(now), request.targetProfileId());
        jdbc.update("update model_release_records set status='rolled_back', updated_at=? where id=?", Timestamp.from(now), releaseId);
        releaseEvent(releaseId, "rolled_back", String.valueOf(before.get("status")), "rolled_back", request.operator(), request.comment(), Map.of(
                "target_version_id", request.targetVersionId(), "target_profile_id", request.targetProfileId()
        ));
        audit("model_release", releaseId, "rolled_back", request.operator(), before, Map.of(
                "target_version_id", request.targetVersionId(), "target_profile_id", request.targetProfileId()), request.comment());
        return release(releaseId);
    }

    public List<Map<String, Object>> auditLogs(String entityType, String entityId) {
        List<Object> args = new ArrayList<>();
        StringBuilder where = new StringBuilder(" where 1=1 ");
        if (entityType != null && !entityType.isBlank()) {
            where.append(" and entity_type=? ");
            args.add(entityType);
        }
        if (entityId != null && !entityId.isBlank()) {
            where.append(" and entity_id=? ");
            args.add(entityId);
        }
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select id, entity_type, entity_id, action, operator_name,
                       before_snapshot::text as before_snapshot, after_snapshot::text as after_snapshot,
                       reason, created_at from model_audit_logs
                """ + where + " order by created_at desc limit 200", args.toArray());
        return parseRows(rows, "before_snapshot", "after_snapshot");
    }

    private Map<String, Object> parameterProfile(String profileId) {
        try {
            return parseRow(jdbc.queryForMap("""
                    select p.id, p.model_group_id, p.model_version_id, v.version as model_version,
                           p.name, p.revision, p.scope_type, p.scope_value::text as scope_value,
                           p.parameters::text as parameters, p.based_on_id, p.status, p.change_reason,
                           p.created_by, p.created_at, p.updated_at
                    from model_parameter_profiles p join model_versions v on v.id=p.model_version_id where p.id=?
                    """, profileId), "scope_value", "parameters");
        } catch (Exception e) {
            throw notFound("参数方案不存在");
        }
    }

    private Map<String, Object> versionRow(String versionId) {
        try {
            return parseRow(jdbc.queryForMap("""
                    select v.id, v.model_group, v.version, v.status, v.metrics::text as metrics,
                           mg.id as model_group_id, mg.model_type
                    from model_versions v join model_groups mg on mg.code=v.model_group where v.id=?
                    """, versionId), "metrics");
        } catch (Exception e) {
            throw notFound("模型版本不存在");
        }
    }

    private Map<String, Object> requireModelGroup(String modelGroupId) {
        try {
            return jdbc.queryForMap("""
                    select id, code, name, model_type, description, input_modalities::text as input_modalities_text,
                           owner_team, enabled from model_groups where id=?
                    """, modelGroupId);
        } catch (Exception e) {
            throw notFound("模型组不存在");
        }
    }

    private String modelGroupCode(String modelGroupId) {
        return String.valueOf(requireModelGroup(modelGroupId).get("code"));
    }

    private void requireGroupVersion(String modelGroupId, Map<String, Object> version) {
        if (!Objects.equals(version.get("model_group_id"), modelGroupId)) {
            throw badRequest("模型版本不属于当前模型组");
        }
    }

    private List<Map<String, Object>> uploadFiles(String sessionId) {
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select id, session_id, file_name, content_type, modality, size_bytes, sha256,
                       object_key, storage_url, metadata::text as metadata, validation_status,
                       validation_messages::text as validation_messages, created_at
                from test_upload_files where session_id=? order by created_at, id
                """, sessionId);
        return parseRows(rows, "metadata", "validation_messages");
    }

    private Map<String, Object> chunkedManifest(String manifestId) {
        Map<String, Object> row;
        try {
            row = jdbc.queryForMap("""
                    select m.id, m.session_id, m.file_name, m.content_type, m.total_size_bytes,
                           m.part_size_bytes, m.total_parts, m.expected_sha256, m.status,
                           m.upload_file_id, m.created_at, m.completed_at,
                           count(p.id) as uploaded_part_count,
                           coalesce(sum(p.size_bytes), 0) as uploaded_size_bytes
                    from test_upload_manifests m
                    left join test_upload_parts p on p.manifest_id=m.id
                    where m.id=? group by m.id
                    """, manifestId);
        } catch (Exception e) {
            throw notFound("分片上传清单不存在");
        }
        row = new HashMap<>(row);
        row.put("uploaded_parts", jdbc.queryForList(
                "select part_index from test_upload_parts where manifest_id=? order by part_index", Integer.class, manifestId));
        return row;
    }

    private Map<String, Object> uploadFile(String fileId) {
        try {
            return parseRow(jdbc.queryForMap("""
                    select id, session_id, file_name, content_type, modality, size_bytes, sha256,
                           object_key, storage_url, metadata::text as metadata, validation_status,
                           validation_messages::text as validation_messages, created_at
                    from test_upload_files where id=?
                    """, fileId), "metadata", "validation_messages");
        } catch (Exception e) {
            throw notFound("上传文件不存在");
        }
    }

    private void insertTestInput(
            String taskId,
            String sourceType,
            String sampleId,
            String uploadFileId,
            String name,
            String storageUrl,
            String sha256,
            String modality,
            Map<String, Object> metadata,
            Map<String, Object> groundTruth,
            String validationStatus,
            List<?> validationMessages
    ) {
        jdbc.update("""
                        insert into model_test_inputs(
                            id, test_task_id, source_type, sample_id, upload_file_id, name, storage_url,
                            sha256, modality, metadata_snapshot, ground_truth_snapshot, validation_status,
                            validation_messages, created_at
                        ) values (?,?,?,?,?,?,?,?,?,?::jsonb,?::jsonb,?,?::jsonb,?)
                        """,
                Ids.next("mti"), taskId, sourceType, sampleId, uploadFileId, name, storageUrl,
                sha256, modality, Jsonb.write(metadata), Jsonb.write(groundTruth), validationStatus,
                Jsonb.write(validationMessages), Timestamp.from(Instant.now()));
    }

    private Map<String, Object> currentProduction(String modelGroupId) {
        List<Map<String, Object>> rows = jdbc.queryForList("""
                select r.model_version_id, r.parameter_profile_id
                from model_release_records r where r.model_group_id=? and r.status='production'
                order by r.effective_at desc nulls last, r.created_at desc limit 1
                """, modelGroupId);
        return rows.isEmpty() ? Map.of("model_version_id", "", "parameter_profile_id", "") : rows.get(0);
    }

    private Map<String, Object> release(String releaseId) {
        return listReleases(null).stream().filter(row -> Objects.equals(row.get("id"), releaseId)).findFirst()
                .orElseThrow(() -> notFound("发布记录不存在"));
    }

    private void requireReleaseStatus(Map<String, Object> release, Set<String> allowed) {
        if (!allowed.contains(String.valueOf(release.get("status")))) {
            throw conflict("发布记录当前状态不允许执行该操作");
        }
    }

    private void releaseEvent(
            String releaseId,
            String action,
            String fromStatus,
            String toStatus,
            String operator,
            String comment,
            Map<String, Object> payload
    ) {
        jdbc.update("""
                insert into model_release_events(id, release_id, action, from_status, to_status, operator_name, comment, payload, created_at)
                values (?,?,?,?,?,?,?,?::jsonb,?)
                """, Ids.next("release-event"), releaseId, action, fromStatus, toStatus,
                defaultString(operator, "模型发布中心"), comment, Jsonb.write(payload), Timestamp.from(Instant.now()));
    }

    private void audit(
            String entityType,
            String entityId,
            String action,
            String operator,
            Map<String, Object> before,
            Map<String, Object> after,
            String reason
    ) {
        jdbc.update("""
                insert into model_audit_logs(id, entity_type, entity_id, action, operator_name, before_snapshot, after_snapshot, reason, created_at)
                values (?,?,?,?,?,?::jsonb,?::jsonb,?,?)
                """, Ids.next("audit"), entityType, entityId, action, defaultString(operator, "系统"),
                Jsonb.write(before), Jsonb.write(after), reason, Timestamp.from(Instant.now()));
    }

    private Map<String, Object> uploadMetadata(String modality, String fileName, boolean localFallback, String modelType) {
        Map<String, Object> metadata = new LinkedHashMap<>();
        metadata.put("original_file_name", fileName);
        metadata.put("storage_mode", localFallback ? "local_fallback" : "object_storage");
        metadata.put("uploaded_at", Instant.now().toString());
        metadata.put("scene", switch (modelType) {
            case "thermal" -> "接头严重发热";
            case "pointcloud" -> "防洪点精密监测";
            case "change" -> "彩钢板清理";
            case "segmentation" -> "裂缝识别";
            default -> "塔吊";
        });
        if ("thermal".equals(modality)) {
            metadata.put("radiometric", fileName.toLowerCase(Locale.ROOT).endsWith(".tiff"));
            metadata.put("temperature_c", 82.6);
        }
        if ("pointcloud".equals(modality)) {
            metadata.put("point_count", 680000);
            metadata.put("crs", "EPSG:4547");
            metadata.put("volume_change_m3", 1.6);
        }
        return metadata;
    }

    private String detectModality(String fileName, String contentType) {
        String lower = fileName.toLowerCase(Locale.ROOT);
        if (lower.endsWith(".mp4") || lower.endsWith(".mov") || lower.endsWith(".mkv")) {
            return "video";
        }
        if (lower.endsWith(".pcd") || lower.endsWith(".ply") || lower.endsWith(".las") || lower.endsWith(".laz")) {
            return "pointcloud";
        }
        if (lower.endsWith(".tif") || lower.endsWith(".tiff") || lower.endsWith(".geotiff") || lower.endsWith(".dem")) {
            return lower.contains("thermal") || lower.contains("infrared") || lower.contains("红外") ? "thermal" : "raster";
        }
        if (contentType != null && contentType.startsWith("video/")) {
            return "video";
        }
        return "image";
    }

    private String safeFileName(String value) {
        String sanitized = value.replaceAll("[\\\\/:*?\"<>|]", "_").replaceAll("\\s+", "_");
        if (sanitized.isBlank()) {
            return "uploaded-file";
        }
        return sanitized.length() > 180 ? sanitized.substring(sanitized.length() - 180) : sanitized;
    }

    private double estimatedSeconds(Map<String, Object> group) {
        return switch (String.valueOf(group.get("model_type"))) {
            case "pointcloud" -> 12;
            case "change" -> 8;
            case "segmentation" -> 3;
            default -> 2;
        };
    }

    private List<Map<String, Object>> parseRows(List<Map<String, Object>> rows, String... jsonFields) {
        return rows.stream().map(row -> parseRow(row, jsonFields)).toList();
    }

    private Map<String, Object> parseRow(Map<String, Object> source, String... jsonFields) {
        Map<String, Object> row = new HashMap<>(source);
        for (String field : jsonFields) {
            Object value = row.get(field);
            if (value != null && !(value instanceof Map<?, ?>) && !(value instanceof List<?>)) {
                row.put(field, Jsonb.read(String.valueOf(value)));
            }
        }
        return row;
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> castMap(Object value) {
        if (value instanceof Map<?, ?> raw) {
            Map<String, Object> result = new LinkedHashMap<>();
            raw.forEach((key, item) -> result.put(String.valueOf(key), item));
            return result;
        }
        return Map.of();
    }

    @SuppressWarnings("unchecked")
    private List<Map<String, Object>> castMapList(Object value) {
        if (!(value instanceof List<?> list)) {
            return List.of();
        }
        return list.stream().filter(Map.class::isInstance).map(item -> castMap(item)).toList();
    }

    private List<?> castList(Object value) {
        return value instanceof List<?> list ? list : List.of();
    }

    private Map<String, Object> defaultMap(Map<String, Object> value) {
        return value == null ? Map.of() : value;
    }

    private List<String> safeList(List<String> value) {
        return value == null ? List.of() : value.stream().filter(Objects::nonNull).filter(item -> !item.isBlank()).distinct().toList();
    }

    private String defaultString(Object value, String fallback) {
        return value == null || String.valueOf(value).isBlank() ? fallback : String.valueOf(value);
    }

    private String markdown(Object value) {
        return String.valueOf(value == null ? "-" : value).replace("|", "\\|").replace("\r", " ").replace("\n", " ");
    }

    private double number(Object value) {
        return value instanceof Number number ? number.doubleValue() : 0;
    }

    private ResponseStatusException notFound(String message) {
        return new ResponseStatusException(HttpStatus.NOT_FOUND, message);
    }

    private ResponseStatusException badRequest(String message) {
        return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
    }

    private ResponseStatusException conflict(String message) {
        return new ResponseStatusException(HttpStatus.CONFLICT, message);
    }

    public record UploadedContent(byte[] content, String contentType, String fileName) {
    }

    public record TestReport(String fileName, String content) {
    }
}
