<template>
  <div v-loading="loading" class="model-test-workbench">
    <template v-if="!task">
      <div class="test-config-strip">
        <div class="test-type-control">
          <span>测试类型</span>
          <el-radio-group v-model="testType" size="large">
            <el-radio-button value="quick">单样本快速测试</el-radio-button>
            <el-radio-button value="regression">批量回归</el-radio-button>
            <el-radio-button value="version_compare">版本对比</el-radio-button>
            <el-radio-button value="parameter_compare">参数对比</el-radio-button>
          </el-radio-group>
        </div>
        <div class="test-config-fields">
          <label><span>任务名称</span><el-input v-model="taskName" maxlength="80" /></label>
          <label>
            <span>模型版本</span>
            <el-select v-model="selectedVersionIds" :multiple="testType === 'version_compare'" collapse-tags :max-collapse-tags="2">
              <el-option v-for="version in versions" :key="version.id" :label="`${version.version} · ${version.status}`" :value="version.id" />
            </el-select>
          </label>
          <label>
            <span>参数方案</span>
            <el-select v-model="selectedProfileIds" :multiple="testType === 'parameter_compare'" collapse-tags :max-collapse-tags="2">
              <el-option v-for="profile in selectableProfiles" :key="profile.id" :label="`${profile.name} r${profile.revision} · ${profile.model_version}`" :value="profile.id" />
            </el-select>
          </label>
          <label v-if="testType === 'regression'">
            <span>固定回归集</span>
            <el-select v-model="selectedSampleSetId" clearable placeholder="选择回归集" @change="applySampleSet">
              <el-option v-for="set in sampleSets" :key="String(set.id)" :label="`${set.name} · ${set.version}`" :value="String(set.id)" />
            </el-select>
          </label>
        </div>
      </div>

      <el-tabs v-model="inputMode" class="test-input-tabs">
        <el-tab-pane name="library">
          <template #label><span class="tab-label"><el-icon><Collection /></el-icon>样本库选择 <b>{{ selectedSamples.length }}</b></span></template>
          <div class="sample-selector-layout">
            <section class="sample-browser">
              <div class="sample-filter-bar">
                <el-input v-model="sampleFilters.keyword" clearable placeholder="样本名称、场景或线路" :prefix-icon="Search" @keyup.enter="loadSamples" />
                <el-select v-model="sampleFilters.modality" clearable placeholder="数据模态" @change="loadSamples">
                  <el-option v-for="modality in group.input_modalities" :key="modality" :label="modalityLabel(modality)" :value="modality" />
                </el-select>
                <el-select v-model="sampleFilters.difficulty" clearable placeholder="难例标签" @change="loadSamples">
                  <el-option v-for="tag in difficultyOptions" :key="tag" :label="tag" :value="tag" />
                </el-select>
                <el-button :icon="Search" @click="loadSamples">检索</el-button>
              </div>
              <el-table ref="sampleTable" :data="samples" height="410" row-key="id" @selection-change="onSampleSelection">
                <el-table-column type="selection" width="44" :reserve-selection="true" />
                <el-table-column label="测试样本" min-width="230">
                  <template #default="scope"><button class="sample-name-cell sample-name-preview" type="button" @click.stop="previewLibrarySample(scope.row)"><i :class="`sample-modality ${scope.row.modality}`"></i><span><strong>{{ scope.row.name }}</strong><small>{{ scope.row.id }} · {{ scope.row.source }}</small></span></button></template>
                </el-table-column>
                <el-table-column prop="scene" label="场景" min-width="130" />
                <el-table-column label="线路位置" min-width="145"><template #default="scope">{{ scope.row.line_id || "-" }}<small class="cell-subtext">{{ scope.row.mileage || "-" }}</small></template></el-table-column>
                <el-table-column label="模态" width="90"><template #default="scope"><el-tag effect="plain" size="small">{{ modalityLabel(scope.row.modality) }}</el-tag></template></el-table-column>
                <el-table-column label="标注" width="90"><template #default="scope"><el-tag :type="scope.row.label_status === 'reviewed' ? 'success' : 'warning'" effect="plain" size="small">{{ scope.row.label_status === "reviewed" ? "已复核" : scope.row.label_status }}</el-tag></template></el-table-column>
                <el-table-column label="难例" min-width="150"><template #default="scope"><el-tag v-for="tag in scope.row.difficulty_tags" :key="tag" class="difficulty-tag" type="info" size="small" effect="plain">{{ tag }}</el-tag></template></el-table-column>
                <el-table-column label="预览" width="64" fixed="right"><template #default="scope"><el-tooltip content="预览样本" placement="top"><el-button aria-label="预览样本" :icon="View" circle size="small" @click.stop="previewLibrarySample(scope.row)" /></el-tooltip></template></el-table-column>
              </el-table>
            </section>
            <aside class="selected-input-tray">
              <header><strong>已选输入</strong><span>{{ selectedSamples.length }} 项</span></header>
              <div v-if="selectedSamples.length" class="selected-input-list">
                <div v-for="sample in selectedSamples" :key="sample.id" class="selected-input-item">
                  <i :class="`sample-modality ${sample.modality}`"></i><button class="selected-input-preview" type="button" @click="previewLibrarySample(sample)"><strong>{{ sample.name }}</strong><small>{{ sample.scene }} · {{ modalityLabel(sample.modality) }}</small></button><span class="selected-input-actions"><el-tooltip content="预览样本" placement="top"><el-button aria-label="预览样本" :icon="View" circle size="small" @click="previewLibrarySample(sample)" /></el-tooltip><el-tooltip content="移除样本" placement="top"><el-button aria-label="移除样本" :icon="Close" circle size="small" @click="removeSelectedSample(sample)" /></el-tooltip></span>
                </div>
              </div>
              <el-empty v-else description="从左侧选择测试样本" :image-size="60" />
              <dl class="input-statistics"><div><dt>真值覆盖</dt><dd>{{ groundTruthCoverage }}%</dd></div><div><dt>数据模态</dt><dd>{{ selectedModalities }}</dd></div><div><dt>推理次数</dt><dd>{{ estimatedInferenceCount }}</dd></div></dl>
            </aside>
          </div>
        </el-tab-pane>

        <el-tab-pane name="upload">
          <template #label><span class="tab-label"><el-icon><UploadFilled /></el-icon>本地上传 <b>{{ uploadSession?.file_count || localFiles.length }}</b></span></template>
          <div class="upload-test-layout">
            <el-upload
              class="test-upload-dropzone"
              drag
              multiple
              :auto-upload="false"
              :on-change="onLocalFilesChanged"
              :on-remove="onLocalFilesChanged"
              :on-preview="previewLocalFile"
              :disabled="uploading"
            >
              <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
              <div class="el-upload__text">拖入测试文件，或点击选择</div>
              <template #tip><div class="el-upload__tip">支持 {{ acceptedFormats }}，文件仅进入隔离测试存储，默认保留 7 天</div></template>
            </el-upload>
            <aside class="upload-session-panel">
              <header><strong>上传会话</strong><el-tag v-if="uploadSession" :type="uploadSession.status === 'completed' ? 'success' : 'warning'" effect="plain">{{ uploadSession.status }}</el-tag></header>
              <template v-if="uploadSession">
                <dl><div><dt>会话编号</dt><dd>{{ uploadSession.id }}</dd></div><div><dt>文件数量</dt><dd>{{ uploadSession.file_count }}</dd></div><div><dt>总大小</dt><dd>{{ formatBytes(uploadSession.total_size_bytes) }}</dd></div><div><dt>存储方式</dt><dd>{{ uploadSession.storage_mode === "object_storage" ? "对象存储" : "本地容错存储" }}</dd></div></dl>
                <ul class="uploaded-file-list"><li v-for="file in uploadSession.files" :key="file.id"><el-icon><Document /></el-icon><button type="button" @click="previewUploadedFile(file)"><strong>{{ file.file_name }}</strong><small>{{ modalityLabel(file.modality) }} · {{ formatBytes(file.size_bytes) }}</small></button><span class="uploaded-file-actions"><el-tag :type="statusTagType(file.validation_status)" size="small">{{ file.validation_status === "valid" ? "有效" : "不兼容" }}</el-tag><el-tooltip content="预览上传样本" placement="top"><el-button aria-label="预览上传样本" :icon="View" circle size="small" @click="previewUploadedFile(file)" /></el-tooltip></span></li></ul>
              </template>
              <el-empty v-else description="选择文件后建立上传会话" :image-size="56" />
              <el-progress v-if="uploading" :percentage="uploadProgress" />
              <el-button type="primary" :icon="Upload" :loading="uploading" :disabled="!localFiles.length" @click="uploadLocalFiles">上传并校验</el-button>
            </aside>
          </div>
        </el-tab-pane>
      </el-tabs>

      <section class="test-budget-bar">
        <div><span>输入数量</span><strong>{{ totalInputCount }}</strong></div>
        <div><span>测试组合</span><strong>{{ matrix.length }}</strong></div>
        <div><span>预计推理</span><strong>{{ estimatedInferenceCount }}</strong></div>
        <div><span>资源隔离</span><strong>测试资源池</strong></div>
        <p v-if="validationResult" :class="validationResult.valid ? 'valid' : 'invalid'">
          <el-icon><CircleCheck v-if="validationResult.valid" /><Warning v-else /></el-icon>
          {{ validationResult.valid ? `校验通过，预计 ${validationResult.budget.estimated_minutes} 分钟` : `校验失败，${validationResult.errors.length} 项需处理` }}
        </p>
        <span class="filter-spacer"></span>
        <el-button :icon="DocumentChecked" :disabled="!canCreate" @click="createAndValidate(false)">校验配置</el-button>
        <el-button type="primary" :icon="VideoPlay" :disabled="!canCreate" :loading="starting" @click="createAndValidate(true)">启动测试</el-button>
      </section>
    </template>

    <template v-else>
      <header class="running-test-header">
        <div><el-tag :type="statusTagType(task.status)" effect="dark">{{ testStatusLabel(task.status) }}</el-tag><span><strong>{{ task.name }}</strong><small>{{ task.task_no }} · {{ task.trace_id }}</small></span></div>
        <div class="list-actions"><el-button v-if="isRunning" :icon="Close" type="danger" plain @click="cancelCurrentTest">取消</el-button><el-button :icon="CopyDocument" @click="cloneCurrentTest">复制任务</el-button><el-button type="primary" :icon="Plus" @click="resetWorkbench">新建测试</el-button></div>
      </header>
      <el-progress :percentage="Number(task.progress)" :status="task.status === 'failed' ? 'exception' : completed ? 'success' : undefined" :stroke-width="12" />

      <div class="test-step-track">
        <div v-for="(step, index) in testSteps" :key="step.key" :class="stepClass(step.key)">
          <i><el-icon v-if="stepCompleted(step.key)"><Check /></el-icon><span v-else>{{ index + 1 }}</span></i>
          <span><strong>{{ step.label }}</strong><small>{{ stepMessage(step.key) }}</small></span>
        </div>
      </div>

      <div v-if="!completed" class="execution-monitor">
        <section class="execution-current">
          <div class="processing-visual"><span></span><span></span><span></span><i></i></div>
          <strong>{{ latestEvent?.message || "等待任务事件" }}</strong>
          <p>{{ task.current_step }} · {{ task.progress }}%</p>
          <dl><div><dt>输入数量</dt><dd>{{ task.inputs.length }}</dd></div><div><dt>测试组合</dt><dd>{{ task.matrix_snapshot.length }}</dd></div><div><dt>运行队列</dt><dd>model-test</dd></div></dl>
        </section>
        <section class="execution-events"><header><strong>运行事件</strong><span>{{ task.events.length }} 条</span></header><ol><li v-for="event in [...task.events].reverse().slice(0, 12)" :key="event.event_id"><time>{{ eventTime(event.created_at) }}</time><i :class="event.status"></i><span><strong>{{ event.message }}</strong><small>{{ event.step_key }} · {{ event.event_type }}</small></span></li></ol></section>
      </div>

      <template v-else>
        <div class="test-summary-strip">
          <div v-for="summary in summaries" :key="summary.id" class="test-summary-item">
            <el-tag :type="summary.acceptance_status === 'passed' ? 'success' : 'danger'" effect="dark">{{ summary.acceptance_status === "passed" ? "通过" : "未通过" }}</el-tag>
            <span><strong>{{ summary.model_version }} · {{ summary.parameter_profile_name || "临时参数" }}</strong><small>{{ summary.conclusion }}</small></span>
            <dl><div v-for="([key, value]) in Object.entries(summary.metrics).slice(0, 4)" :key="key"><dt>{{ metricLabel(key) }}</dt><dd>{{ metricValue(key, value) }}</dd></div></dl>
          </div>
        </div>

        <div v-if="results.length" class="test-result-layout">
          <aside class="result-item-list">
            <header><strong>结果明细</strong><span>{{ results.length }} 项</span></header>
            <div class="result-list-filters"><el-checkbox v-model="onlyFailed">仅异常项</el-checkbox></div>
            <button v-for="result in filteredResults" :key="result.id" :class="{ active: selectedResult?.id === result.id }" @click="selectedResult = result">
              <i :class="`sample-modality ${result.modality}`"></i><span><strong>{{ result.input_name }}</strong><small>{{ result.model_version }} · {{ result.status === "success" ? `${result.timings.total_ms} ms` : result.error_code }}</small></span><el-tag :type="statusTagType(result.status)" size="small">{{ result.status === "success" ? "成功" : "失败" }}</el-tag>
            </button>
          </aside>
          <ModelResultViewer v-if="selectedResult" :result="selectedResult" />
          <aside v-if="selectedResult" class="result-detail-panel">
            <el-tabs v-model="resultDetailTab">
              <el-tab-pane label="指标" name="metrics"><dl class="result-metric-list"><div v-for="([key, value]) in Object.entries(selectedResult.metrics)" :key="key"><dt>{{ metricLabel(key) }}</dt><dd>{{ metricValue(key, value) }}</dd></div></dl></el-tab-pane>
              <el-tab-pane label="参数" name="parameters"><pre class="detail-json">{{ JSON.stringify(selectedResult.parameter_snapshot, null, 2) }}</pre></el-tab-pane>
              <el-tab-pane label="运行" name="runtime"><dl class="result-runtime-list"><div v-for="([key, value]) in Object.entries(selectedResult.runtime_snapshot)" :key="key"><dt>{{ key }}</dt><dd>{{ value }}</dd></div></dl></el-tab-pane>
            </el-tabs>
            <div class="result-feedback-actions"><strong>结果反馈</strong><el-button-group><el-button size="small" @click="feedback('false_positive', true)">误报回流</el-button><el-button size="small" @click="feedback('missed_detection', true)">漏报回流</el-button><el-button size="small" @click="feedback('data_quality', false)">数据问题</el-button></el-button-group></div>
          </aside>
        </div>
        <el-empty v-else description="任务没有生成结果明细" />
      </template>
    </template>

    <SamplePreviewDrawer v-model="previewVisible" :sample="previewSample" />
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import type { UploadFile, UploadFiles, UploadRawFile } from "element-plus";
import { ElMessage } from "element-plus";
import { Check, CircleCheck, Close, Collection, CopyDocument, Document, DocumentChecked, Plus, Search, Upload, UploadFilled, VideoPlay, View, Warning } from "@element-plus/icons-vue";
import SamplePreviewDrawer from "../common/SamplePreviewDrawer.vue";
import ModelResultViewer from "./ModelResultViewer.vue";
import {
  cancelModelTest, cloneModelTest, completeChunkedTestFile, completeTestUploadSession, createChunkedTestFile, createModelTest, createTestUploadSession,
  modelParameterProfiles, modelSampleSets, modelTestDetail, modelTestEventsUrl, modelTestResults, modelTestSamples,
  modelTestSummaries, startModelTest, submitModelTestFeedback, uploadTestFilePart, uploadTestFiles, validateModelTest
} from "../../services/api";
import {
  formatBytes, type JsonMap, type ModelGroup, type ModelSample, type ModelTestTask, type ModelVersion,
  type ParameterProfile, type SamplePreviewData, statusTagType, testStatusLabel, type TestEvent, type TestResult, type TestSummary, type UploadFileRecord, type UploadSession
} from "../../types/model-management";

const props = defineProps<{ group: ModelGroup; versions: ModelVersion[]; initialTaskId?: string }>();
const loading = ref(false);
const starting = ref(false);
const uploading = ref(false);
const uploadProgress = ref(0);
const profiles = ref<ParameterProfile[]>([]);
const samples = ref<ModelSample[]>([]);
const sampleSets = ref<JsonMap[]>([]);
const selectedSamples = ref<ModelSample[]>([]);
const selectedSampleSetId = ref("");
const sampleTable = ref<any>();
const localFiles = ref<UploadRawFile[]>([]);
const uploadSession = ref<UploadSession>();
const inputMode = ref("library");
const testType = ref("quick");
const taskName = ref(`${props.group.name}快速验证`);
const selectedVersionIds = ref<string | string[]>("");
const selectedProfileIds = ref<string | string[]>("");
const sampleFilters = ref({ keyword: "", modality: "", difficulty: "" });
const validationResult = ref<any>();
const task = ref<ModelTestTask>();
const results = ref<TestResult[]>([]);
const summaries = ref<TestSummary[]>([]);
const selectedResult = ref<TestResult>();
const onlyFailed = ref(false);
const resultDetailTab = ref("metrics");
const previewVisible = ref(false);
const previewSample = ref<SamplePreviewData>();
let eventSource: EventSource | undefined;
let pollTimer: number | undefined;
let localPreviewUrl: string | undefined;

const difficultyOptions = ["雨天", "夜间", "小目标", "低对比度", "遮挡", "运动模糊", "复杂纹理", "边界模糊", "远距离", "稀疏边缘", "季节变化"];
const testSteps = [
  { key: "snapshot", label: "快照固化" }, { key: "input_validation", label: "输入校验" },
  { key: "resource_schedule", label: "资源调度" }, { key: "preprocessing", label: "预处理" },
  { key: "inference", label: "模型推理" }, { key: "postprocessing", label: "结果后处理" },
  { key: "metrics", label: "指标计算" }, { key: "archive", label: "结果归档" }
];
const stepOrder = Object.fromEntries(testSteps.map((step, index) => [step.key, index]));

const selectableProfiles = computed(() => {
  const versionIds = Array.isArray(selectedVersionIds.value) ? selectedVersionIds.value : [selectedVersionIds.value];
  return profiles.value.filter((item) => versionIds.includes(item.model_version_id));
});
const matrix = computed(() => {
  const versionIds = Array.isArray(selectedVersionIds.value) ? selectedVersionIds.value : [selectedVersionIds.value].filter(Boolean);
  const profileIds = Array.isArray(selectedProfileIds.value) ? selectedProfileIds.value : [selectedProfileIds.value].filter(Boolean);
  if (testType.value === "parameter_compare") {
    return profileIds.map((profileId) => {
      const profile = profiles.value.find((item) => item.id === profileId);
      return { model_version_id: profile?.model_version_id || versionIds[0], parameter_profile_id: profileId, temporary_parameters: {} };
    });
  }
  return versionIds.map((versionId) => ({
    model_version_id: versionId,
    parameter_profile_id: profileIds.find((id) => profiles.value.find((profile) => profile.id === id)?.model_version_id === versionId)
      || profiles.value.find((profile) => profile.model_version_id === versionId && profile.status !== "inactive")?.id || "",
    temporary_parameters: {}
  }));
});
const totalInputCount = computed(() => selectedSamples.value.length + Number(uploadSession.value?.file_count || 0));
const estimatedInferenceCount = computed(() => totalInputCount.value * Math.max(1, matrix.value.length));
const canCreate = computed(() => Boolean(taskName.value.trim() && totalInputCount.value && matrix.value.length && matrix.value.every((item) => item.model_version_id && item.parameter_profile_id)));
const groundTruthCoverage = computed(() => selectedSamples.value.length ? Math.round(selectedSamples.value.filter((item) => Object.keys(item.ground_truth || {}).length).length * 100 / selectedSamples.value.length) : 0);
const selectedModalities = computed(() => [...new Set(selectedSamples.value.map((item) => modalityLabel(item.modality)))].join("、") || (uploadSession.value?.files.map((item) => modalityLabel(item.modality)).join("、") || "-") );
const acceptedFormats = computed(() => props.group.model_type === "pointcloud" ? "PCD、PLY、LAS、LAZ" : props.group.model_type === "thermal" ? "辐射 JPG、TIFF" : props.group.model_type === "change" ? "JPG、PNG、TIF、GeoTIFF、DEM" : "JPG、PNG、TIFF、MP4、MOV");
const latestEvent = computed<TestEvent | undefined>(() => task.value?.events?.[task.value.events.length - 1]);
const isRunning = computed(() => task.value && ["validating", "queued", "running", "cancelling"].includes(task.value.status));
const completed = computed(() => task.value && ["completed", "partially_completed", "failed", "cancelled", "rejected"].includes(task.value.status));
const filteredResults = computed(() => onlyFailed.value ? results.value.filter((item) => item.status !== "success") : results.value);

async function initialize() {
  loading.value = true;
  try {
    profiles.value = await modelParameterProfiles(props.group.id);
    const candidate = props.versions.find((item) => item.status === "candidate") || props.versions[0];
    selectedVersionIds.value = candidate?.id || "";
    selectedProfileIds.value = profiles.value.find((item) => item.model_version_id === candidate?.id && item.status !== "inactive")?.id || "";
    await loadSamples();
    sampleSets.value = await modelSampleSets(props.group.id);
    if (props.initialTaskId) await openTask(props.initialTaskId);
  } finally { loading.value = false; }
}

async function applySampleSet(sampleSetId: string) {
  const sampleSet = sampleSets.value.find((item) => item.id === sampleSetId);
  const ids = new Set<string>((sampleSet?.sample_ids as string[] | undefined) || []);
  selectedSamples.value = samples.value.filter((sample) => ids.has(sample.id));
  await nextTick();
  (sampleTable.value as any)?.clearSelection?.();
  selectedSamples.value.forEach((sample) => (sampleTable.value as any)?.toggleRowSelection?.(sample, true));
}

async function loadSamples() {
  samples.value = await modelTestSamples({ modelGroupId: props.group.id, ...sampleFilters.value });
}

function onSampleSelection(rows: ModelSample[]) {
  selectedSamples.value = testType.value === "quick" && rows.length > 1 ? [rows[rows.length - 1]] : rows;
}

function removeSelectedSample(sample: ModelSample) {
  selectedSamples.value = selectedSamples.value.filter((item) => item.id !== sample.id);
  (sampleTable.value as any)?.toggleRowSelection?.(sample, false);
}

function previewLibrarySample(sample: ModelSample) {
  releaseLocalPreviewUrl();
  previewSample.value = {
    ...sample,
    preview_url: browserPreviewUrl(sample.thumbnail_url) || browserPreviewUrl(sample.storage_url),
    metadata: sample.metadata || {},
    ground_truth: sample.ground_truth || {}
  };
  previewVisible.value = true;
}

function previewLocalFile(file: UploadFile) {
  releaseLocalPreviewUrl();
  const raw = file.raw;
  if (raw) localPreviewUrl = URL.createObjectURL(raw);
  const modality = fileModality(file.name, raw?.type || "");
  previewSample.value = {
    id: String(file.uid),
    name: file.name,
    modality,
    preview_url: ["image", "video", "thermal", "raster"].includes(modality) ? localPreviewUrl : undefined,
    source: "本地待上传",
    size_bytes: raw?.size,
    content_type: raw?.type,
    label_status: "待测试",
    quality_status: "待校验",
    metadata: { file_name: file.name, content_type: raw?.type || "application/octet-stream", last_modified: raw?.lastModified },
    ground_truth: {}
  };
  previewVisible.value = true;
}

function previewUploadedFile(file: UploadFileRecord) {
  releaseLocalPreviewUrl();
  previewSample.value = {
    id: file.id,
    name: file.file_name,
    modality: file.modality,
    preview_url: ["image", "video", "thermal", "raster"].includes(file.modality) ? `/api/v1/test-upload-files/${file.id}/content` : undefined,
    storage_url: String(file.storage_url || ""),
    source: "测试上传会话",
    size_bytes: file.size_bytes,
    content_type: file.content_type,
    quality_status: file.validation_status,
    validation_messages: file.validation_messages,
    metadata: file.metadata || {},
    ground_truth: {}
  };
  previewVisible.value = true;
}

function browserPreviewUrl(value: unknown) {
  const url = String(value || "");
  return /^(https?:|data:|blob:|\/api\/)/.test(url) ? url : undefined;
}

function fileModality(name: string, contentType: string) {
  const lower = name.toLowerCase();
  if (contentType.startsWith("video/") || /\.(mp4|mov|avi|mkv)$/.test(lower)) return "video";
  if (/\.(pcd|ply|las|laz)$/.test(lower)) return "pointcloud";
  if (/\.(tif|tiff|geotiff|dem)$/.test(lower)) return props.group.model_type === "thermal" ? "thermal" : "raster";
  if (props.group.model_type === "thermal") return "thermal";
  return "image";
}

function releaseLocalPreviewUrl() {
  if (localPreviewUrl) URL.revokeObjectURL(localPreviewUrl);
  localPreviewUrl = undefined;
}

function onLocalFilesChanged(_file: UploadFile, files: UploadFiles) {
  localFiles.value = files.map((item) => item.raw).filter((item): item is UploadRawFile => Boolean(item));
  validationResult.value = undefined;
}

async function uploadLocalFiles() {
  if (!localFiles.value.length) return;
  uploading.value = true;
  uploadProgress.value = 0;
  try {
    const session = await createTestUploadSession(props.group.id);
    uploadSession.value = session;
    const partSize = 8 * 1024 * 1024;
    const directFiles = localFiles.value.filter((file) => file.size <= partSize);
    const chunkedFiles = localFiles.value.filter((file) => file.size > partSize);
    const totalBytes = localFiles.value.reduce((sum, file) => sum + file.size, 0);
    let completedBytes = 0;
    if (directFiles.length) {
      const directBytes = directFiles.reduce((sum, file) => sum + file.size, 0);
      await uploadTestFiles(session.id, directFiles, (percent) => {
        uploadProgress.value = Math.round((completedBytes + directBytes * percent / 100) * 100 / totalBytes);
      });
      completedBytes += directBytes;
    }
    for (const file of chunkedFiles) {
      const manifest = await createChunkedTestFile(session.id, file, partSize);
      const uploaded = new Set<number>(manifest.uploaded_parts || []);
      for (let index = 0; index < manifest.total_parts; index++) {
        const start = index * manifest.part_size_bytes;
        const end = Math.min(file.size, start + manifest.part_size_bytes);
        if (!uploaded.has(index)) {
          await uploadPartWithRetry(session.id, manifest.id, index, file.slice(start, end));
        }
        uploadProgress.value = Math.round((completedBytes + end) * 100 / totalBytes);
      }
      await completeChunkedTestFile(session.id, manifest.id);
      completedBytes += file.size;
    }
    uploadSession.value = await completeTestUploadSession(session.id);
    ElMessage.success(`已上传并校验 ${uploadSession.value?.file_count || 0} 个文件`);
  } finally { uploading.value = false; }
}

async function uploadPartWithRetry(sessionId: string, manifestId: string, partIndex: number, chunk: Blob) {
  let lastError: unknown;
  for (let attempt = 0; attempt < 3; attempt++) {
    try {
      await uploadTestFilePart(sessionId, manifestId, partIndex, chunk);
      return;
    } catch (error) {
      lastError = error;
      await new Promise((resolve) => window.setTimeout(resolve, 400 * (attempt + 1)));
    }
  }
  throw lastError;
}

async function createAndValidate(runAfterValidation: boolean) {
  if (!canCreate.value) return;
  starting.value = true;
  try {
    const created = await createModelTest({
      name: taskName.value,
      test_type: testType.value,
      model_group_id: props.group.id,
      matrix: matrix.value,
      inputs: { library_sample_ids: selectedSamples.value.map((item) => item.id), upload_session_ids: uploadSession.value ? [uploadSession.value.id] : [] },
      options: { save_intermediate: true, priority: "normal", result_retention_days: 30 },
      operator: "测试人员"
    });
    const validation = await validateModelTest(created.id);
    validationResult.value = validation;
    if (!validation.valid) {
      ElMessage.error(`测试配置存在 ${validation.errors.length} 项错误`);
      return;
    }
    if (!runAfterValidation) {
      ElMessage.success("测试配置校验通过，已生成任务草稿");
      return;
    }
    const started = await startModelTest(created.id);
    task.value = started.task;
    connectEvents(task.value!.id);
    beginPolling();
  } finally { starting.value = false; }
}

async function openTask(taskId: string) {
  task.value = await modelTestDetail(taskId);
  if (completed.value) await loadResults();
  else { connectEvents(taskId); beginPolling(); }
}

function connectEvents(taskId: string) {
  eventSource?.close();
  const last = task.value?.events?.at(-1)?.sequence_no || 0;
  eventSource = new EventSource(modelTestEventsUrl(taskId, last));
  ["step.completed", "test.queued", "test.validating", "step.progress", "item.completed", "item.failed", "test.completed", "test.failed", "test.cancelled", "test.rejected"].forEach((name) => eventSource?.addEventListener(name, onSseEvent as EventListener));
  eventSource.onerror = () => { if (!completed.value) beginPolling(); };
}

function onSseEvent(event: MessageEvent) {
  try {
    const data = JSON.parse(event.data) as TestEvent;
    if (task.value && !task.value.events.some((item) => item.sequence_no === data.sequence_no)) task.value.events.push(data);
    void refreshTask();
  } catch { /* keep polling as fallback */ }
}

function beginPolling() {
  if (pollTimer) return;
  pollTimer = window.setInterval(refreshTask, 700);
}

async function refreshTask() {
  if (!task.value) return;
  const latest = await modelTestDetail(task.value.id);
  task.value = latest;
  if (completed.value) {
    stopConnections();
    await loadResults();
  }
}

async function loadResults() {
  if (!task.value) return;
  [results.value, summaries.value] = await Promise.all([modelTestResults(task.value.id), modelTestSummaries(task.value.id)]);
  selectedResult.value = results.value.find((item) => item.status === "success") || results.value[0];
}

async function cancelCurrentTest() {
  if (!task.value) return;
  task.value = await cancelModelTest(task.value.id);
  ElMessage.success("已提交取消请求");
}

async function cloneCurrentTest() {
  if (!task.value) return;
  const cloned = await cloneModelTest(task.value.id);
  resetWorkbench();
  ElMessage.success(`已复制为任务 ${cloned.task_no}`);
}

async function feedback(type: string, archive: boolean) {
  if (!selectedResult.value) return;
  await submitModelTestFeedback(selectedResult.value.id, type, archive, archive ? "加入候选样本并进入后续标注复核" : "标记输入数据质量问题");
  ElMessage.success(archive ? "结果已回流候选样本" : "数据问题已记录");
}

function resetWorkbench() {
  stopConnections();
  task.value = undefined;
  results.value = [];
  summaries.value = [];
  validationResult.value = undefined;
  selectedSamples.value = [];
  uploadSession.value = undefined;
  localFiles.value = [];
  selectedSampleSetId.value = "";
  taskName.value = `${props.group.name}快速验证`;
}

function stepCompleted(key: string) {
  const currentIndex = stepOrder[task.value?.current_step || ""] ?? -1;
  return task.value?.events.some((event) => event.step_key === key && ["completed", "success"].includes(event.status)) || currentIndex > stepOrder[key] || completed.value;
}
function stepClass(key: string) { return { completed: stepCompleted(key), active: task.value?.current_step === key, failed: task.value?.status === "failed" && task.value?.current_step === key }; }
function stepMessage(key: string) { return [...(task.value?.events || [])].reverse().find((event) => event.step_key === key)?.message || (stepCompleted(key) ? "已完成" : "等待执行"); }
function modalityLabel(value: string) { return ({ image: "图片", video: "视频", thermal: "红外", pointcloud: "点云", raster: "栅格" } as Record<string, string>)[value] || value; }
function metricLabel(value: string) { return ({ precision: "精确率", recall: "召回率", f1: "F1", map50: "mAP50", miou: "mIoU", dice: "Dice", boundary_f1: "边界 F1", temperature_error_c: "温度误差", hotspot_recall: "热点召回", registration_error_m: "配准误差", anomaly_recall: "异常召回", iou: "IoU", average_confidence: "平均置信度", result_count: "结果数量", p95_latency_ms: "P95 耗时", failure_rate: "失败率", sample_count: "样本量" } as Record<string, string>)[value] || value; }
function metricValue(key: string, value: unknown) { const number = Number(value); if (Number.isNaN(number)) return String(value); if (["precision", "recall", "f1", "map50", "miou", "dice", "boundary_f1", "hotspot_recall", "anomaly_recall", "iou", "average_confidence", "failure_rate"].includes(key)) return `${(number * 100).toFixed(1)}%`; if (key.includes("latency")) return `${number.toFixed(0)} ms`; if (key.includes("error_m")) return `${number.toFixed(3)} m`; if (key.includes("temperature")) return `${number.toFixed(2)}°C`; return Number.isInteger(number) ? String(number) : number.toFixed(3); }
function eventTime(value: string) { return new Date(value).toLocaleTimeString("zh-CN", { hour12: false }); }
function stopConnections() { eventSource?.close(); eventSource = undefined; if (pollTimer) window.clearInterval(pollTimer); pollTimer = undefined; }

watch(testType, async (value) => {
  validationResult.value = undefined;
  taskName.value = `${props.group.name}${({ quick: "快速验证", regression: "批量回归", version_compare: "版本对比", parameter_compare: "参数对比" } as Record<string, string>)[value]}`;
  if (value !== "regression") selectedSampleSetId.value = "";
  if (value === "quick" && selectedSamples.value.length > 1) {
    selectedSamples.value = selectedSamples.value.slice(0, 1);
    await nextTick();
    (sampleTable.value as any)?.clearSelection?.();
    if (selectedSamples.value[0]) (sampleTable.value as any)?.toggleRowSelection?.(selectedSamples.value[0], true);
  }
  const candidate = props.versions.find((item) => item.status === "candidate") || props.versions[0];
  const production = props.versions.find((item) => item.status === "production");
  if (value === "version_compare") selectedVersionIds.value = [production?.id, candidate?.id].filter((item): item is string => Boolean(item));
  else selectedVersionIds.value = candidate?.id || "";
});
watch(selectedVersionIds, () => {
  const versionIds = Array.isArray(selectedVersionIds.value) ? selectedVersionIds.value : [selectedVersionIds.value];
  const available = profiles.value.filter((item) => versionIds.includes(item.model_version_id) && item.status !== "inactive");
  selectedProfileIds.value = testType.value === "parameter_compare" ? available.slice(0, 2).map((item) => item.id) : available[0]?.id || "";
}, { deep: true });
onMounted(initialize);
onBeforeUnmount(() => { stopConnections(); releaseLocalPreviewUrl(); });
</script>
