﻿<template>
  <div class="page-shell module-page">
    <BrandTopBar active="pricing" />

    <div class="main-content">
      <aside class="left-panel">
        <!-- 运行模式 -->
        <div class="section-title">跟价模式</div>
        <div class="mode-zone">
          <label class="mode-check-row">
            <input type="radio" name="price-track-mode" class="mode-check-input" :checked="statusModeEnabled" @change="selectMode('status')" />
            <span class="mode-check-text">按商品状态（全量）</span>
            <span class="mode-hint-inline">— 获取店铺所有在售ASIN跑跟价</span>
          </label>
          <label class="mode-check-row">
            <input type="radio" name="price-track-mode" class="mode-check-input" :checked="asinModeEnabled" @change="selectMode('asin')" />
            <span class="mode-check-text">按指定ASIN文档（自定义）</span>
            <span class="mode-hint-inline">— 上传ASIN表格，按文档内ASIN跑跟价</span>
          </label>
        </div>

        <!-- 按指定ASIN文档：上传 -->
        <div class="section-title">执行配置</div>
        <div class="mode-zone">
          <label class="mode-check-row">
            <input v-model="executionMode" type="radio" class="mode-check-input" value="FINITE" />
            <span class="mode-check-text">执行固定轮次</span>
            <span class="mode-hint-inline">按整批店铺重复执行指定次数</span>
          </label>
          <label class="mode-check-row">
            <input v-model="executionMode" type="radio" class="mode-check-input" value="INFINITE" />
            <span class="mode-check-text">无限执行</span>
            <span class="mode-hint-inline">上一轮全部成功后立即开始下一轮</span>
          </label>
          <div v-if="executionMode === 'FINITE'" class="loop-round-input">
            <span class="hint">轮次</span>
            <el-input-number v-model="roundCount" :min="1" :max="9999" :disabled="hasRunningLoop || pushing" />
          </div>
        </div>

        <template v-if="asinModeEnabled">
          <div class="section-title">ASIN文档 <span class="tag-badge">自定义模式</span></div>
          <div class="upload-zone">
            <div class="hint">上传包含ASIN的表格文件，按文档内的ASIN进行跟价。</div>
            <div class="btns">
              <button type="button" class="opt-btn" @click="selectAsinFile">选择ASIN文件</button>
              <button type="button" class="opt-btn" @click="selectAsinFolder">选择文件夹</button>
            </div>
            <div class="selected-files clean-placeholder">
              <template v-if="asinFiles.length">
                <span v-for="(f, i) in asinFiles" :key="i">{{ f }}</span>
              </template>
              <span v-else class="text-muted">暂未选择</span>
            </div>
          </div>
        </template>

        <template v-if="statusModeEnabled">

        <!-- 店铺输入 -->
        <div class="section-title">店铺名称</div>
        <div class="input-zone">
          <div class="hint">输入店铺名后点「确认」加入下方备选区。可同时配合上方模式使用。</div>
          <div class="input-row">
            <el-input v-model="shopInput" clearable placeholder="请输入店铺名称" @keyup.enter="confirmAdd" />
            <button type="button" class="opt-btn" :disabled="adding" @click="confirmAdd">
              {{ adding ? '保存中…' : '确认' }}
            </button>
          </div>
        </div>

        <!-- 备选区域 -->
        <div class="section-title">备选店铺</div>
        <div v-if="!candidates.length" class="empty-candidates">暂无备选店铺，请先输入并确认</div>
        <div v-else class="candidate-table-scroll">
          <el-table :data="candidates" row-key="id" height="160" class="candidate-table"
            @selection-change="onSelectionChange">
            <el-table-column type="selection" width="42" />
            <el-table-column prop="shopName" label="店铺名" min-width="120" show-overflow-tooltip />
            <el-table-column label="操作" width="72" align="center">
              <template #default="{ row }">
                <button type="button" class="link-danger" @click="removeCandidate(row)">删除</button>
              </template>
            </el-table-column>
          </el-table>
        </div>
        </template>

        <!-- 国家顺序 -->
        <div class="section-title">处理国家与顺序</div>
        <p class="hint country-pref-hint">
          勾选需要处理的站点；拖拽调整顺序。设置会按当前登录用户保存。
        </p>
        <div class="country-pref-checks">
          <label v-for="row in countryCheckboxRows" :key="row.code" class="country-check-row">
            <input type="checkbox" class="country-check-input" :checked="isCountrySelected(row.code)"
              :disabled="isCountrySelectionLocked(row.code)"
              @change="onCountryNativeChange(row.code, $event)" />
            <span class="country-check-text">{{ row.label }}（{{ row.code }}）</span>
          </label>
        </div>
        <div v-if="orderedCountryCodes.length" class="country-order-panel">
          <div class="country-order-caption">已选顺序（拖拽 ⋮⋮ 调整）</div>
          <div class="country-order-list">
            <div v-for="(code, idx) in orderedCountryCodes" :key="code" class="country-drag-row"
              :class="{ dragging: dragCountryIndex === idx }" draggable="true" @dragstart="onCountryDragStart(idx)"
              @dragend="onCountryDragEnd" @dragover.prevent @drop.prevent="onCountryDrop(idx)">
              <span class="drag-handle" title="拖动排序">⋮⋮</span>
              <span class="country-drag-label">{{ countryLabel(code) }}（{{ code }}）</span>
            </div>
          </div>
        </div>
        <div v-if="countryPrefSaving" class="country-pref-status">保存中…</div>

        <!-- 状态指示 -->
        <div v-if="statusModeEnabled" class="mode-status-bar">
          <span class="mode-status-dot active"></span>
          <span class="mode-status-text">商品状态模式：将在售（Active）ASIN纳入跟价，跳过ASIN由后台配置的店铺维度数据提供</span>
        </div>

        <div class="run-row">
          <button type="button" class="btn-run" :disabled="matching" @click="runMatch">
            {{ matching ? '匹配中…' : '匹配店铺' }}
          </button>
          <button type="button" class="btn-run btn-queue" :disabled="pushing || !hasValidMode" @click="pushToPythonLoopQueue">
            {{ pushing ? '推送中…' : '推送到 Python 队列' }}
          </button>
        </div>
        <p class="loading-msg">上传ASIN文档和多选店铺<strong>可同时生效</strong>，跟价结果合并输出。全量和自定义的区别仅在于ASIN来源不同。</p>

        <div v-if="activeLoopRun" class="loop-status-card">
          <div class="loop-status-line">循环任务 ID：{{ activeLoopRun.id }}</div>
          <div class="loop-status-line">状态：{{ activeLoopRun.status || '-' }}</div>
          <div class="loop-status-line">进度：{{ loopProgressText }}</div>
          <div class="loop-status-line">当前店铺：{{ ((activeLoopRun.currentShopIndex || 0) + 1) }}/{{ activeLoopRun.totalShopCount || 0 }}</div>
          <div v-if="activeLoopRun.errorMessage" class="loop-status-line loop-error">{{ activeLoopRun.errorMessage }}</div>
          <button type="button" class="opt-btn loop-stop-btn" :disabled="!hasRunningLoop || stopLoopPending" @click="stopLoopExecution">
            {{ stopLoopPending ? '停止中...' : '停止循环' }}
          </button>
        </div>

        <div v-if="queuePushResult" class="queue-debug-card">
          <div class="section-title queue-debug-title">推送结果</div>
          <div class="queue-debug-line">{{ queuePushResult }}</div>
          <pre v-if="queuePayloadText" class="queue-debug-payload">{{ queuePayloadText }}</pre>
        </div>
      </aside>

      <section class="right-panel">
        <div class="panel-header">匹配与任务</div>
        <div class="task-list-wrap">
          <div class="clean-result-summary pr-summary">
            <div class="summary-card">
              <span class="summary-label">备选店铺数</span>
              <strong>{{ dashboard.candidateCount }}</strong>
            </div>
            <div class="summary-card">
              <span class="summary-label">已结束任务</span>
              <strong>{{ dashboard.processedTaskCount }}</strong>
            </div>
            <div class="summary-card">
              <span class="summary-label">成功任务</span>
              <strong>{{ dashboard.successTaskCount }}</strong>
            </div>
            <div class="summary-card">
              <span class="summary-label">失败任务</span>
              <strong>{{ dashboard.failedTaskCount }}</strong>
            </div>
          </div>

          <div class="subsection-title">匹配结果</div>
          <div v-if="!matchedItems.length" class="empty-tasks narrow">完成「匹配店铺」后，在此展示紫鸟索引匹配结果，供核对后再推送队列。</div>
          <el-table v-else :data="matchedItems" :row-key="rowKeyForMatch" :highlight-current-row="false"
            class="result-table match-table">
            <el-table-column prop="shopName" label="店铺名" min-width="100" />
            <el-table-column label="匹配" width="72" align="center">
              <template #default="{ row }">
                <span :class="row.matched ? 'ok' : 'fail'">{{ row.matched ? '是' : '否' }}</span>
              </template>
            </el-table-column>
            <el-table-column prop="shopId" label="店铺ID" min-width="120" show-overflow-tooltip />
            <el-table-column prop="platform" label="平台" width="88" show-overflow-tooltip />
            <el-table-column prop="companyName" label="公司" min-width="100" show-overflow-tooltip />
            <el-table-column label="状态" width="100" show-overflow-tooltip>
              <template #default="{ row }">{{ formatMatchStatus(row.matchStatus) }}</template>
            </el-table-column>
            <el-table-column label="说明" min-width="140" show-overflow-tooltip>
              <template #default="{ row }">{{ formatMatchRemark(row) }}</template>
            </el-table-column>
            <el-table-column label="操作" width="72" align="center">
              <template #default="{ row }">
                <button type="button" class="link-danger" @click="removeMatchedRow(row)">删除</button>
              </template>
            </el-table-column>
          </el-table>

          <div class="result-list-wrap">
            <div class="result-list-header">
              <span>处理记录</span>
            </div>

            <div v-if="!hasVisibleList" class="empty-tasks">暂无处理记录。推送队列并等待 Python 回传后，将在此显示下载链接。</div>

            <template v-else>
              <div v-if="currentSectionItems.length" class="result-subsection">
                <div class="result-subsection-title">当前任务</div>
                <ul class="task-list clean-result-list">
                  <li v-for="item in currentSectionItems" :key="`cur-${item.resultId}-${item.taskId}`"
                    class="task-item split-result-item">
                    <div class="left split-result-main">
                      <div v-if="item.roundIndex" class="files">轮次：第 {{ item.roundIndex }} 轮</div>
                      <span class="id" :title="item.shopName || ''">{{ item.shopName || '-' }}</span>
                      <div class="files">任务 ID：{{ item.taskId ?? '-' }}</div>
                      <div v-if="item.platform" class="files">平台：{{ item.platform }}</div>
                      <div v-if="item.error" class="files">错误：{{ item.error }}</div>
                    </div>
                    <div class="task-right split-result-actions">
                      <span class="status" :class="statusClass(item)">{{ statusText(item) }}</span>
                      <button v-if="canDownload(item)" type="button" class="download" @click="downloadResult(item)">
                        下载
                      </button>
                      <button type="button" class="btn-delete" @click="deleteTaskRecord(item)">删除</button>
                    </div>
                  </li>
                </ul>
              </div>

              <div v-if="historySectionItems.length" class="result-subsection">
                <div class="result-subsection-title">历史记录</div>
                <ul class="task-list clean-result-list">
                  <li v-for="item in historySectionItems" :key="`his-${item.resultId}-${item.taskId}`"
                    class="task-item split-result-item">
                    <div class="left split-result-main">
                      <div v-if="item.roundIndex" class="files">轮次：第 {{ item.roundIndex }} 轮</div>
                      <span class="id" :title="item.shopName || ''">{{ item.shopName || '-' }}</span>
                      <div class="files">任务 ID：{{ item.taskId ?? '-' }}</div>
                      <div v-if="item.outputFilename" class="files">文件：{{ item.outputFilename }}</div>
                      <div v-if="item.error" class="files">错误：{{ item.error }}</div>
                    </div>
                    <div class="task-right split-result-actions">
                      <span class="status" :class="statusClass(item)">{{ statusText(item) }}</span>
                      <button v-if="canDownload(item)" type="button" class="download" @click="downloadResult(item)">
                        下载
                      </button>
                      <button type="button" class="btn-delete" @click="deleteTaskRecord(item)">删除</button>
                    </div>
                  </li>
                </ul>
              </div>
            </template>
          </div>
        </div>
      </section>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
import { expandBrandFolderRecursive } from '@/shared/api/brand'
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
import {
  addPriceTrackCandidate,
  completePriceTrackLoopChild,
  createPriceTrackTask,
  createPriceTrackLoopRun,
  dispatchNextPriceTrackLoopRun,
  deletePendingPriceTrackShopResult,
  deletePriceTrackCandidate,
  deletePriceTrackHistory,
  deletePriceTrackTask,
  getPriceTrackCountryPreference,
  getPriceTrackDashboard,
  getPriceTrackHistory,
  getPriceTrackLoopRun,
  getPriceTrackResultDownloadUrl,
  getPriceTrackTaskProgressBatch,
  getPriceTrackTasksBatch,
  listPriceTrackCandidates,
  matchPriceTrackShops,
  putPriceTrackCountryPreference,
  stopPriceTrackLoopRun,
  type PriceTrackAsinParsedRow,
  type PriceTrackCandidateVo,
  type PriceTrackCreateTaskVo,
  type PriceTrackDashboardVo,
  type PriceTrackExecutionMode,
  type PriceTrackHistoryItem,
  type PriceTrackLoopRunVo,
  type PriceTrackShopQueueItem,
  type PriceTrackTaskDetailVo,
} from '@/shared/api/java-modules'

// ========== 类型定义 ==========
const COUNTRY_OPTIONS = [
  { code: 'DE', label: '德国' },
  { code: 'UK', label: '英国' },
  { code: 'FR', label: '法国' },
  { code: 'IT', label: '意大利' },
  { code: 'ES', label: '西班牙' },
] as const

// ========== 状态 ==========
const shopInput = ref('')
const candidates = ref<PriceTrackCandidateVo[]>([])
const selectedCandidates = ref<PriceTrackCandidateVo[]>([])
const matchedItems = ref<PriceTrackShopQueueItem[]>([])
const adding = ref(false)
const matching = ref(false)
const pushing = ref(false)
const queueWorkerRunning = ref(false)
const autoQueueEnabled = ref(false)
const queuePushResult = ref('')
const queuePayloadText = ref('')
const matchAsinRowsByCountry = ref<Record<string, PriceTrackAsinParsedRow[]>>({})
const executionMode = ref<PriceTrackExecutionMode>('FINITE')
const roundCount = ref(1)
const activeLoopRunId = ref<number | null>(null)
const activeLoopRun = ref<PriceTrackLoopRunVo | null>(null)
const loopDispatching = ref(false)
const stopLoopPending = ref(false)

// 跟价模式（可同时启用）
const statusModeEnabled = ref(true)   // 按商品状态（全量）
const asinModeEnabled = ref(false)    // 按指定ASIN文档（自定义）

// 上传文件
const asinFiles = ref<string[]>([])        // ASIN文档
const asinUploadedFiles = ref<UploadedJavaFile[]>([])

// 是否有有效模式
const hasValidMode = computed(() => statusModeEnabled.value || asinModeEnabled.value)
const loopIsTerminal = computed(() => {
  const status = activeLoopRun.value?.status || ''
  return status === 'SUCCESS' || status === 'FAILED' || status === 'STOPPED'
})
const hasRunningLoop = computed(() => !!activeLoopRunId.value && !loopIsTerminal.value)
const loopProgressText = computed(() => {
  const loop = activeLoopRun.value
  if (!loop) return ''
  const round = loop.currentRound || 1
  if (loop.executionMode === 'INFINITE') return `第 ${round} 轮（无限）`
  const total = Math.max(1, loop.targetRounds || roundCount.value || 1)
  return `第 ${round}/${total} 轮`
})

const orderedCountryCodes = ref<string[]>(['DE', 'UK', 'FR', 'IT', 'ES'])
const dragCountryIndex = ref<number | null>(null)
const countryPrefSaving = ref(false)
const countryPrefUserTouched = ref(false)
let countryPrefSaveTimer: ReturnType<typeof setTimeout> | null = null

const dashboard = ref<PriceTrackDashboardVo>({
  candidateCount: 0,
  processedTaskCount: 0,
  successTaskCount: 0,
  failedTaskCount: 0,
})

const historyItems = ref<PriceTrackHistoryItem[]>([])
const pollingTaskIds = ref<number[]>([])
const taskDetails = ref<Record<number, string>>({})
const taskSnapshots = ref<Record<number, PriceTrackTaskDetailVo>>({})
const pollTimer = ref<number | null>(null)
const pollingInFlight = ref(false)

function uidForStorage() {
  return typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
}

function sessionKey() {
  return `price-track:tasks:${uidForStorage()}`
}

function taskStatusStorageKey() {
  return `price-track:task-status:${uidForStorage()}`
}

function taskSnapshotsStorageKey() {
  return `price-track:tasks-batch-snapshot:${uidForStorage()}`
}

function matchedItemsStorageKey() {
  return `price-track:matched-items:${uidForStorage()}`
}

function activeLoopStorageKey() {
  return `price-track:active-loop:${uidForStorage()}`
}

function setStorageJson(key: string, value: unknown, shouldRemove: boolean) {
  if (typeof window === 'undefined') return
  try {
    if (shouldRemove) window.localStorage.removeItem(key)
    else window.localStorage.setItem(key, JSON.stringify(value))
  } catch {
    /* quota */
  }
}

function loadPollingIdsFromStorage() {
  try {
    const raw = typeof window !== 'undefined' ? window.localStorage.getItem(sessionKey()) : null
    if (!raw) {
      pollingTaskIds.value = []
      return
    }
    const parsed = JSON.parse(raw) as unknown
    pollingTaskIds.value = Array.isArray(parsed) ? parsed.filter((n): n is number => typeof n === 'number' && n > 0) : []
  } catch {
    pollingTaskIds.value = []
  }
}

function savePollingIds() {
  setStorageJson(sessionKey(), pollingTaskIds.value, pollingTaskIds.value.length === 0)
}

function loadTaskDetailsFromStorage() {
  try {
    const raw = typeof window !== 'undefined' ? window.localStorage.getItem(taskStatusStorageKey()) : null
    if (!raw) return
    const parsed = JSON.parse(raw) as Record<string, unknown>
    const out: Record<number, string> = {}
    for (const [k, v] of Object.entries(parsed)) {
      const id = Number(k)
      if (id > 0 && typeof v === 'string') out[id] = v
    }
    taskDetails.value = out
  } catch {
    /* ignore */
  }
}

function saveTaskDetailsToStorage() {
  setStorageJson(taskStatusStorageKey(), taskDetails.value, Object.keys(taskDetails.value).length === 0)
}

function loadTaskSnapshotsFromStorage() {
  try {
    const raw = typeof window !== 'undefined' ? window.localStorage.getItem(taskSnapshotsStorageKey()) : null
    if (!raw) return
    const parsed = JSON.parse(raw) as Record<string, unknown>
    const out: Record<number, PriceTrackTaskDetailVo> = {}
    for (const [k, v] of Object.entries(parsed)) {
      const id = Number(k)
      if (id > 0 && v && typeof v === 'object') out[id] = v as PriceTrackTaskDetailVo
    }
    taskSnapshots.value = out
  } catch {
    /* ignore */
  }
}

function saveTaskSnapshotsToStorage() {
  setStorageJson(taskSnapshotsStorageKey(), taskSnapshots.value, Object.keys(taskSnapshots.value).length === 0)
}

function loadMatchedItemsFromStorage() {
  try {
    const raw = typeof window !== 'undefined' ? window.localStorage.getItem(matchedItemsStorageKey()) : null
    if (!raw) return
    const parsed = JSON.parse(raw) as unknown
    if (Array.isArray(parsed)) matchedItems.value = parsed as PriceTrackShopQueueItem[]
  } catch {
    /* ignore */
  }
}

function saveMatchedItemsToStorage() {
  setStorageJson(matchedItemsStorageKey(), matchedItems.value, matchedItems.value.length === 0)
}

function loadActiveLoopRunIdFromStorage() {
  try {
    const raw = typeof window !== 'undefined' ? window.localStorage.getItem(activeLoopStorageKey()) : null
    if (!raw) {
      activeLoopRunId.value = null
      return
    }
    const parsed = Number(JSON.parse(raw))
    activeLoopRunId.value = Number.isFinite(parsed) && parsed > 0 ? parsed : null
  } catch {
    activeLoopRunId.value = null
  }
}

function saveActiveLoopRunId() {
  setStorageJson(activeLoopStorageKey(), activeLoopRunId.value, !activeLoopRunId.value)
}

// ========== 文件上传 ==========
async function selectAsinFile() {
  const api = getPywebviewApi()
  if (api?.select_files) {
    const result = await api.select_files({
      filters: [{ name: 'Excel/CSV', extensions: 'xlsx,xls,csv' }],
      multiple: true,
    })
    if (result?.paths?.length) {
      asinFiles.value = result.paths
      asinUploadedFiles.value = await uploadAsinPathsToJava(result.paths)
      ElMessage.success(`已选择 ${result.paths.length} 个ASIN文件`)
    }
    return
  }
  if (api?.select_brand_xlsx_files) {
    const paths = await api.select_brand_xlsx_files()
    if (!paths?.length) return
    asinFiles.value = paths
    asinUploadedFiles.value = await uploadAsinPathsToJava(paths)
    ElMessage.success(`已选择 ${paths.length} 个ASIN文件`)
    return
  }
  ElMessage.warning('当前环境不支持文件选择，请在本地客户端中打开')
}

async function selectAsinFolder() {
  const api = getPywebviewApi()
  if (!api?.select_brand_folder) {
    ElMessage.warning('当前环境不支持文件夹选择，请在本地客户端中打开')
    return
  }
  try {
    const folder = await api.select_brand_folder()
    if (!folder) return
    const result = await expandBrandFolderRecursive(folder)
    if (!result.success || !result.items?.length) {
      ElMessage.warning(result.error || '该文件夹下没有可用的 xlsx 文件')
      return
    }
    asinFiles.value = result.items.map((item) => item.relativePath || item.absolutePath)
    asinUploadedFiles.value = await uploadAsinPathsToJava(result.items)
    ElMessage.success(`已选择文件夹内 ${result.items.length} 个ASIN文件`)
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '选择失败')
  }
}



// ========== 国家顺序相关 ==========
async function uploadAsinPathsToJava(paths: Array<string | { absolutePath: string; relativePath?: string }>) {
  const api = getPywebviewApi()
  if (!api?.upload_file_to_java) {
    return []
  }
  const uploaded: UploadedJavaFile[] = []
  for (const item of paths) {
    const filePath = typeof item === 'string' ? item : item.absolutePath
    const relativePath = typeof item === 'string' ? undefined : item.relativePath
    const result = await api.upload_file_to_java(filePath, relativePath)
    if (!result?.success || !result.data) {
      throw new Error(result?.error || result?.message || `上传失败：${filePath}`)
    }
    uploaded.push(result.data)
  }
  return uploaded
}

function resolveAsinRequestPaths() {
  if (asinUploadedFiles.value.length) {
    return asinUploadedFiles.value.map((item) => item.localPath).filter((path) => !!path)
  }
  return asinFiles.value
}

function selectMode(mode: 'status' | 'asin') {
  statusModeEnabled.value = mode === 'status'
  asinModeEnabled.value = mode === 'asin'
}

function countryLabel(code: string) {
  const row = COUNTRY_OPTIONS.find((o) => o.code === code)
  return row?.label ?? code
}

const countryCheckboxRows = computed(() => {
  const sel = orderedCountryCodes.value
  const selectedSet = new Set(sel)
  const selectedPart = sel.map((code) => ({ code, label: countryLabel(code) }))
  const rest = COUNTRY_OPTIONS.filter((o) => !selectedSet.has(o.code)).map((o) => ({ code: o.code, label: o.label }))
  return [...selectedPart, ...rest]
})

function isCountrySelected(code: string) {
  return orderedCountryCodes.value.includes(code)
}

function isCountrySelectionLocked(code: string) {
  return orderedCountryCodes.value.length === 1 && orderedCountryCodes.value[0] === code
}

function onCountryNativeChange(code: string, e: Event) {
  const el = e.target as HTMLInputElement | null
  if (!el) return
  if (!el.checked && isCountrySelectionLocked(code)) {
    el.checked = true
    ElMessage.warning('至少保留 1 个国家')
    return
  }
  onCountryToggle(code, el.checked)
}

function onCountryToggle(code: string, checked: boolean) {
  countryPrefUserTouched.value = true
  if (checked) {
    if (!orderedCountryCodes.value.includes(code)) {
      orderedCountryCodes.value = [...orderedCountryCodes.value, code]
    }
  } else {
    if (orderedCountryCodes.value.length <= 1) {
      ElMessage.warning('至少保留 1 个国家')
      return
    }
    orderedCountryCodes.value = orderedCountryCodes.value.filter((c) => c !== code)
  }
  scheduleSaveCountryPref()
}

function onCountryDragStart(index: number) {
  dragCountryIndex.value = index
}

function onCountryDragEnd() {
  dragCountryIndex.value = null
}

function onCountryDrop(toIndex: number) {
  const from = dragCountryIndex.value
  dragCountryIndex.value = null
  if (from == null || from === toIndex) return
  countryPrefUserTouched.value = true
  const arr = [...orderedCountryCodes.value]
  const [item] = arr.splice(from, 1)
  arr.splice(toIndex, 0, item)
  orderedCountryCodes.value = arr
  scheduleSaveCountryPref()
}

function scheduleSaveCountryPref() {
  if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer)
  countryPrefSaveTimer = setTimeout(() => {
    countryPrefSaveTimer = null
    void persistCountryPref()
  }, 450)
}

async function persistCountryPref() {
  if (orderedCountryCodes.value.length < 1) return
  countryPrefSaving.value = true
  try {
    await putPriceTrackCountryPreference(orderedCountryCodes.value)
  } catch (e) {
    ElMessage.error(e instanceof Error ? e.message : '保存国家顺序失败')
  } finally {
    countryPrefSaving.value = false
  }
}

async function loadCountryPreference() {
  try {
    const vo = await getPriceTrackCountryPreference()
    if (vo?.countryCodes?.length) {
      orderedCountryCodes.value = [...vo.countryCodes]
    }
  } catch {
    /* ignore */
  }
}

// ========== 店铺备选 ==========
async function loadCandidates() {
  try {
    candidates.value = await listPriceTrackCandidates()
  } catch (e) {
    ElMessage.error(e instanceof Error ? e.message : '加载备选失败')
  }
}

async function loadDashboard() {
  try {
    dashboard.value = await getPriceTrackDashboard()
  } catch {
    /* ignore */
  }
}

async function loadHistory() {
  try {
    const vo = await getPriceTrackHistory()
    historyItems.value = vo.items || []
  } catch {
    historyItems.value = []
  }
}

function syncPollingIdsWithHistory() {
  for (const taskId of [...pollingTaskIds.value]) {
    const any = historyItems.value.some((r) => r.taskId === taskId)
    if (!any && !taskSnapshots.value[taskId]) {
      removePollingTask(taskId)
    }
  }
}

function onSelectionChange(rows: PriceTrackCandidateVo[]) {
  selectedCandidates.value = rows || []
}

async function confirmAdd() {
  const name = shopInput.value.trim()
  if (!name) {
    ElMessage.warning('请输入店铺名称')
    return
  }
  adding.value = true
  try {
    await addPriceTrackCandidate(name)
    shopInput.value = ''
    await loadCandidates()
    ElMessage.success('已加入备选')
  } catch (e) {
    ElMessage.error(e instanceof Error ? e.message : '保存失败')
  } finally {
    adding.value = false
  }
}

async function removeCandidate(row: PriceTrackCandidateVo) {
  try {
    await deletePriceTrackCandidate(row.id)
    await loadCandidates()
    matchedItems.value = []
    saveMatchedItemsToStorage()
    ElMessage.success('已删除')
  } catch (e) {
    ElMessage.error(e instanceof Error ? e.message : '删除失败')
  }
}

// ========== 匹配 & 推送 ==========
function collectShopNamesForMatch(): string[] {
  const set = new Set<string>()
  const extra = shopInput.value.trim()
  if (extra) set.add(extra)
  for (const r of selectedCandidates.value) {
    if (r?.shopName?.trim()) set.add(r.shopName.trim())
  }
  if (asinModeEnabled.value) {
    for (const filePath of asinFiles.value) {
      const normalizedPath = (filePath || '').trim()
      if (!normalizedPath) continue
      const filename = normalizedPath.split(/[/\\]/).filter(Boolean).pop() || ''
      const shopName = filename.replace(/\.[^.]+$/, '').trim()
      if (shopName) set.add(shopName)
    }
  }
  return [...set]
}

async function runMatch() {
  const names = collectShopNamesForMatch()
  if (!names.length) {
    ElMessage.warning('请勾选备选店铺，或在输入框中填写店名')
    return
  }
  if (asinModeEnabled.value && !asinFiles.value.length) {
    ElMessage.warning('请先选择 ASIN 文件')
    return
  }
  matching.value = true
  try {
    const res = await matchPriceTrackShops(names, {
      asinFiles: asinModeEnabled.value ? resolveAsinRequestPaths() : [],
      countryCodes: [...orderedCountryCodes.value],
    })
    const batch = res.items || []
    matchAsinRowsByCountry.value = res.asinRowsByCountry || {}
    const nextByShop = new Map(
      matchedItems.value.map((item) => [((item.shopName || '').trim()), item] as const),
    )
    for (const item of batch) {
      nextByShop.set((item.shopName || '').trim(), item)
    }
    matchedItems.value = [...nextByShop.values()]
    saveMatchedItemsToStorage()
    if (autoQueueEnabled.value) {
      void processMatchedQueue()
    }
    ElMessage.success(`匹配 ${batch.length} 个店铺`)
  } catch (e) {
    ElMessage.error(e instanceof Error ? e.message : '匹配失败')
  } finally {
    matching.value = false
  }
}

function formatMatchStatus(status: string | undefined) {
  const s = (status || '').trim()
  const map: Record<string, string> = { MATCHED: '已匹配', PENDING: '待匹配', CONFLICT: '需人工确认', INDEX_STALE: '索引过期' }
  return map[s] || (s ? s : '—')
}

function formatMatchRemark(row: PriceTrackShopQueueItem) {
  const msg = (row.matchMessage || '').trim()
  if (msg) return msg
  if (row.matched && row.matchStatus === 'MATCHED') return '紫鸟索引已命中，可推送队列'
  if (row.matched) return '已关联索引，请结合状态列查看是否可推送'
  return '未命中或未就绪，请检查店铺名与索引刷新'
}

function rowKeyForMatch(row: { shopName?: string; shopId?: number | string | null }) {
  return `${(row.shopName || '').trim()}\u0001${row.shopId ?? ''}`
}

function isRecordMissingError(error: unknown) {
  const message = error instanceof Error ? error.message : String(error || '')
  return /记录不存在|不存在|已删除|not\s*found|404/i.test(message)
}

function buildSkipAsinDeletePolicy() {
  const enabled = statusModeEnabled.value && !asinModeEnabled.value
  return {
    enabled,
    mode: enabled ? 'DELETE_WHEN_PRICE_BELOW_MINIMUM' : 'NONE',
    deleteWhenPriceBelowMinimum: enabled,
    compareField: 'price',
    thresholdField: 'minimumPrice',
    target: 'skip_asin',
    source: 'frontend-price-track',
  }
}

function removeMatchedRowsLocally(rows: Array<{ shopName?: string; shopId?: number | string | null }>) {
  if (!rows.length) return
  const keys = new Set(rows.map((row) => rowKeyForMatch(row)))
  const names = new Set(rows.map((row) => (row.shopName || '').trim()).filter((name) => !!name))
  matchedItems.value = matchedItems.value.filter((row) => {
    const key = rowKeyForMatch(row)
    const name = (row.shopName || '').trim()
    return !keys.has(key) && (!name || !names.has(name))
  })
  saveMatchedItemsToStorage()
}

function removeHistoryItemLocally(item: PriceTrackHistoryItem) {
  historyItems.value = historyItems.value.filter((row) => {
    if (item.resultId != null && row.resultId === item.resultId) return false
    if (item.taskId != null && row.taskId === item.taskId) return false
    return true
  })
}

async function removeMatchedRow(row: PriceTrackShopQueueItem) {
  const backup = [...matchedItems.value]
  removeMatchedRowsLocally([row])
  const name = (row.shopName || '').trim()
  if (!name) {
    ElMessage.success('已从匹配结果中移除')
    return
  }
  try {
    const res = await deletePendingPriceTrackShopResult(name)
    if (res?.removed) {
      await loadHistory()
      await loadDashboard()
      syncPollingIdsWithHistory()
    }
    ElMessage.success('已从匹配结果中移除')
  } catch (e) {
    if (isRecordMissingError(e)) {
      await loadHistory()
      await loadDashboard()
      syncPollingIdsWithHistory()
      ElMessage.success('后端记录已不存在，已同步移除本地记录')
      return
    }
    matchedItems.value = backup
    saveMatchedItemsToStorage()
    ElMessage.error(e instanceof Error ? e.message : '同步后端失败')
  }
}

async function pushToPythonQueueLegacy() {
  if (!hasValidMode.value) {
    ElMessage.warning('请至少选择一个跟价模式')
    return
  }
  if (asinModeEnabled.value && !asinFiles.value.length) {
    ElMessage.warning('请先选择 ASIN 文件')
    return
  }
  const api = getPywebviewApi()
  if (!api?.enqueue_json) {
    ElMessage.error('当前环境未启用 pywebview enqueue_json')
    return
  }
  const matchedRows = matchedItems.value.filter((i) => i.matched)
  if (!matchedRows.length) {
    ElMessage.warning('无可用匹配店铺')
    return
  }
  pushing.value = true
  queuePayloadText.value = ''
  try {
    // 1. 先调用 Java API 创建任务（落库，含 skip ASIN）
    const taskReq = {
      userId: 0, // 会被 java-modules 自动填充
      statusMode: statusModeEnabled.value,
      asinMode: asinModeEnabled.value,
      items: matchedRows as unknown as Record<string, unknown>[],
      asinFiles: resolveAsinRequestPaths(),
      countryCodes: [...orderedCountryCodes.value],
    }
    const taskVo = await createPriceTrackTask(taskReq)
    taskSnapshots.value = {
      ...taskSnapshots.value,
      [taskVo.taskId]: {
        task: { id: taskVo.taskId, status: 'RUNNING' },
        items: taskVo.items,
      },
    }
    taskDetails.value = {
      ...taskDetails.value,
      [taskVo.taskId]: 'RUNNING',
    }
    saveTaskSnapshotsToStorage()
    saveTaskDetailsToStorage()

    // 2. 推送 taskId 给 Python，Python 会从 Java 拉取完整任务上下文
    const queuePayload = {
      type: 'price-track-run',
      ts: Date.now(),
      data: {
        task_id: taskVo.taskId,
        // 店铺简要信息（用于展示）
        shop_names: matchedRows.map((i) => i.shopName).filter(Boolean),
        country_codes: [...orderedCountryCodes.value],
        skip_asin_delete_policy: buildSkipAsinDeletePolicy(),
        delete_skip_asin_when_price_below_minimum: statusModeEnabled.value && !asinModeEnabled.value,
        deleteSkipAsinWhenPriceBelowMinimum: statusModeEnabled.value && !asinModeEnabled.value,
      },
    }
    queuePayloadText.value = JSON.stringify(queuePayload, null, 2)

    const pushResult = await api.enqueue_json(queuePayload)
    if (pushResult?.success) {
      queuePushResult.value = `任务 ${taskVo.taskId} 已入队，等待执行完成...`
      addPollingTask(taskVo.taskId)
      scheduleNextPoll(true)
      ElMessage.success('已推送到 Python 队列')
    } else {
      queuePushResult.value = `推送失败：${pushResult?.error || '未知错误'}`
      ElMessage.error(queuePushResult.value)
    }
  } catch (e) {
    queuePushResult.value = e instanceof Error ? e.message : '推送异常'
    ElMessage.error(queuePushResult.value)
  } finally {
    pushing.value = false
  }
}

// ========== 任务状态轮询 ==========
function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrackShopQueueItem) {
  const taskItem = taskVo.items?.[0]
  const shopName = (row.shopName || '').trim()
  const skipAsinsByCountry = row.skipAsins || taskVo.skipAsinsByCountry || {}
  const skipAsinDetailsByCountry = taskVo.skipAsinDetailsByCountry || {}
  const minimumPriceByCountryAndAsin = taskVo.minimumPriceByCountryAndAsin || {}
  const skipAsinDeletePolicy = buildSkipAsinDeletePolicy()
  const resultId = taskItem?.resultId ?? null
  const loopRunId = taskItem?.loopRunId ?? null
  const roundIndex = taskItem?.roundIndex ?? null
  const taskStatus = taskItem?.taskStatus || 'RUNNING'
  const success = taskItem?.success ?? false
  const error = taskItem?.error || null
  const outputFilename = taskItem?.outputFilename || null
  const downloadUrl = taskItem?.downloadUrl || null
  return {
    type: 'price-track-run',
    ts: Date.now(),
    data: {
      task_id: taskVo.taskId,
      result_id: resultId,
      shop_name: shopName,
      shop_id: row.shopId ?? null,
      platform: row.platform || '',
      company_name: row.companyName || '',
      shop_mall_name: row.shopMallName || '',
      matched: !!row.matched,
      match_status: row.matchStatus || '',
      match_message: row.matchMessage || null,
      success: success,
      error: error,
      output_filename: outputFilename,
      download_url: downloadUrl,
      task_status: taskStatus,
      loop_run_id: loopRunId,
      round_index: roundIndex,
      country_codes: [...orderedCountryCodes.value],
      mode: statusModeEnabled.value ? 'status' : 'asin',
      skip_asins: skipAsinsByCountry,
      skip_asins_by_country: skipAsinsByCountry,
      skip_asin_details_by_country: skipAsinDetailsByCountry,
      minimum_price_by_country_and_asin: minimumPriceByCountryAndAsin,
      skip_asin_delete_policy: skipAsinDeletePolicy,
      delete_skip_asin_when_price_below_minimum: skipAsinDeletePolicy.deleteWhenPriceBelowMinimum,
      deleteSkipAsinWhenPriceBelowMinimum: skipAsinDeletePolicy.deleteWhenPriceBelowMinimum,
      asin_rows_by_country: Object.keys(matchAsinRowsByCountry.value).length
        ? matchAsinRowsByCountry.value
        : (taskVo.asinRowsByCountry || {}),
      taskId: taskVo.taskId,
      resultId: resultId,
      shopName,
      shopId: row.shopId ?? null,
      platformName: row.platform || '',
      companyName: row.companyName || '',
      shopMallName: row.shopMallName || '',
      matchStatus: row.matchStatus || '',
      matchMessage: row.matchMessage || null,
      outputFilename: outputFilename,
      downloadUrl: downloadUrl,
      taskStatus,
      loopRunId: loopRunId,
      roundIndex: roundIndex,
    },
  }
}

async function waitForTaskTerminal(taskId: number) {
  let transientErrorCount = 0
  const maxTransientErrors = 30
  while (true) {
    try {
      const batch = await getPriceTrackTaskProgressBatch([taskId])
      if (transientErrorCount > 0) {
        queuePushResult.value = `任务 ${taskId} 后端已恢复，继续等待执行结果...`
      }
      transientErrorCount = 0
      if ((batch.missingTaskIds || []).includes(taskId)) {
        removePollingTask(taskId)
        await loadHistory()
        await loadDashboard()
        syncPollingIdsWithHistory()
        return 'FAILED'
      }
      const detail = (batch.items || []).find((item) => item.task?.id === taskId)
      const status = detail?.task?.status || ''
      if (detail) {
        const prev = taskSnapshots.value[taskId]
        taskSnapshots.value = { ...taskSnapshots.value, [taskId]: prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail }
        saveTaskSnapshotsToStorage()
      }
      if (status) {
        taskDetails.value = { ...taskDetails.value, [taskId]: status }
        saveTaskDetailsToStorage()
      }
      if (status === 'SUCCESS' || status === 'FAILED') {
        removePollingTask(taskId)
        await loadHistory()
        await loadDashboard()
        syncPollingIdsWithHistory()
        return status
      }
    } catch (error) {
      const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
      const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
      if (!transient) throw error
      transientErrorCount += 1
      if (transientErrorCount >= maxTransientErrors) {
        throw new Error(`任务 ${taskId} 等待后端恢复超时，请稍后手动刷新查看状态`)
      }
      queuePushResult.value = `任务 ${taskId} 运行中，正在等待后端服务恢复（${transientErrorCount}/${maxTransientErrors}）...`
      if (transientErrorCount === 1 || transientErrorCount % 5 === 0) {
        ElMessage.warning(`任务 ${taskId} 运行中，后端服务暂时不可用，正在自动重试`)
      }
      await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
      continue
    }
    await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
  }
}

async function pushToPythonQueue() {
  autoQueueEnabled.value = true
  await processMatchedQueue()
}

function nextMatchedQueueItem() {
  return matchedItems.value.find((item) => item.matched)
}

async function processMatchedQueue() {
  if (queueWorkerRunning.value) {
    return
  }
  if (!hasValidMode.value) {
    ElMessage.warning('请至少选择一个跟价模式')
    return
  }
  if (asinModeEnabled.value && !asinFiles.value.length) {
    ElMessage.warning('请先选择 ASIN 文件')
    return
  }
  const api = getPywebviewApi()
  if (!api?.enqueue_json) {
    ElMessage.error('当前环境未启用 pywebview enqueue_json')
    return
  }
  const matchedRows = matchedItems.value.filter((i) => i.matched)
  if (!matchedRows.length) {
    ElMessage.warning('无可用匹配店铺')
    return
  }
  queueWorkerRunning.value = true
  pushing.value = true
  queuePayloadText.value = ''
  let successCount = 0
  let failedCount = 0
  try {
    let index = 0
    while (autoQueueEnabled.value) {
      const row = nextMatchedQueueItem()
      if (!row) {
        break
      }
      index += 1
      try {
        let attempt = 0
        const taskVo = await (async () => {
          while (true) {
            try {
              return await createPriceTrackTask({
                userId: 0,
                statusMode: statusModeEnabled.value,
                asinMode: asinModeEnabled.value,
                items: [row] as unknown as Record<string, unknown>[],
                asinFiles: resolveAsinRequestPaths(),
                countryCodes: [...orderedCountryCodes.value],
              })
            } catch (error) {
              const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
              const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
              attempt += 1
              if (!transient || attempt >= 10) throw error
              queuePushResult.value = '后端服务暂时不可用，正在重试创建任务（' + attempt + '/10）...'
              await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
            }
          }
        })()
        taskSnapshots.value = {
          ...taskSnapshots.value,
          [taskVo.taskId]: {
            task: { id: taskVo.taskId, status: 'RUNNING' },
            items: taskVo.items,
          },
        }
        taskDetails.value = {
          ...taskDetails.value,
          [taskVo.taskId]: 'RUNNING',
        }
        saveTaskSnapshotsToStorage()
        saveTaskDetailsToStorage()
        const queuePayload = buildQueuePayload(taskVo, row)
        queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
        const pushResult = await api.enqueue_json(queuePayload)
        if (!pushResult?.success) {
          failedCount += 1
          queuePushResult.value = '任务 ' + taskVo.taskId + ' 推送失败，已自动继续下一条：' + (pushResult?.error || '未知错误')
          ElMessage.error(queuePushResult.value)
          removeMatchedRowsLocally([row])
          continue
        }
        addPollingTask(taskVo.taskId)
        scheduleNextPoll(true)
        removeMatchedRowsLocally([row])
        queuePushResult.value = '任务 ' + taskVo.taskId + ' 已入队，等待执行完成'
        const finalStatus = await waitForTaskTerminal(taskVo.taskId)
        if (finalStatus !== 'SUCCESS') {
          failedCount += 1
          queuePushResult.value = '任务 ' + taskVo.taskId + ' 执行失败，已自动继续下一条'
          ElMessage.error(queuePushResult.value)
          continue
        }
        successCount += 1
        queuePushResult.value = '任务 ' + taskVo.taskId + ' 已完成'
      } catch (error) {
        failedCount += 1
        queuePushResult.value = '第 ' + index + ' 条任务处理失败，已自动继续下一条：' + (error instanceof Error ? error.message : '未知错误')
        ElMessage.error(queuePushResult.value)
        removeMatchedRowsLocally([row])
        continue
      }
    }
    if (successCount > 0 || failedCount > 0) {
      ElMessage.success('店铺任务推送已完成：成功 ' + successCount + ' 条，失败 ' + failedCount + ' 条')
    }
  } catch (e) {
    queuePushResult.value = e instanceof Error ? e.message : '推送异常'
    ElMessage.error(queuePushResult.value)
  } finally {
    queueWorkerRunning.value = false
    pushing.value = false
  }
}

function setLoopRun(next: PriceTrackLoopRunVo | null) {
  activeLoopRun.value = next
  activeLoopRunId.value = next?.id ?? null
  saveActiveLoopRunId()
}

function clearLoopRunIfTerminal() {
  if (!activeLoopRun.value) return
  const status = activeLoopRun.value.status || ''
  if (status === 'SUCCESS' || status === 'FAILED' || status === 'STOPPED') {
    activeLoopRun.value = null
    activeLoopRunId.value = null
    saveActiveLoopRunId()
  }
}

async function syncActiveLoopRun() {
  if (!activeLoopRunId.value) return null
  try {
    const loop = await getPriceTrackLoopRun(activeLoopRunId.value)
    setLoopRun(loop)
    clearLoopRunIfTerminal()
    return activeLoopRun.value
  } catch (error) {
    const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
    const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
    if (transient) return activeLoopRun.value
    activeLoopRun.value = null
    activeLoopRunId.value = null
    saveActiveLoopRunId()
    return null
  }
}

function recordCreatedTask(taskVo: PriceTrackCreateTaskVo) {
  taskSnapshots.value = {
    ...taskSnapshots.value,
    [taskVo.taskId]: {
      task: { id: taskVo.taskId, status: 'RUNNING' },
      items: taskVo.items,
    },
  }
  taskDetails.value = {
    ...taskDetails.value,
    [taskVo.taskId]: 'RUNNING',
  }
  saveTaskSnapshotsToStorage()
  saveTaskDetailsToStorage()
}

async function stopLoopExecution() {
  if (!activeLoopRunId.value || stopLoopPending.value) return
  stopLoopPending.value = true
  try {
    const loop = await stopPriceTrackLoopRun(activeLoopRunId.value)
    setLoopRun(loop)
    clearLoopRunIfTerminal()
    ElMessage.success(loop.status === 'STOPPED' ? '循环已停止' : '已请求停止，当前子任务完成后将收尾')
  } catch (error) {
    ElMessage.error(error instanceof Error ? error.message : '停止循环失败')
  } finally {
    stopLoopPending.value = false
  }
}

async function runLoopExecution(loopId?: number) {
  if (loopDispatching.value) return
  if (loopId && activeLoopRunId.value !== loopId) {
    activeLoopRunId.value = loopId
    saveActiveLoopRunId()
  }
  if (!activeLoopRunId.value) return
  const api = getPywebviewApi()
  if (!api?.enqueue_json) {
    throw new Error('当前环境未启用 pywebview enqueue_json')
  }
  loopDispatching.value = true
  try {
    while (activeLoopRunId.value) {
      const loop = await syncActiveLoopRun()
      if (!loop || loop.status === 'SUCCESS' || loop.status === 'FAILED' || loop.status === 'STOPPED') break

      if (loop.activeTaskId) {
        const activeTaskId = loop.activeTaskId
        addPollingTask(activeTaskId)
        scheduleNextPoll(true)
        const finalStatus = await waitForTaskTerminal(activeTaskId)
        let completeAttempt = 0
        const updatedLoop = await (async () => {
          while (true) {
            try {
              return await completePriceTrackLoopChild(loop.id, activeTaskId)
            } catch (error) {
              const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
              const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
              completeAttempt += 1
              if (!transient || completeAttempt >= 10) throw error
              queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后确认子任务完成（${completeAttempt}/10）...`
              await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
            }
          }
        })()
        setLoopRun(updatedLoop)
        await loadHistory()
        await loadDashboard()
        syncPollingIdsWithHistory()
        clearLoopRunIfTerminal()
        if (finalStatus !== 'SUCCESS') {
          throw new Error(updatedLoop.errorMessage || `任务 ${loop.activeTaskId} 执行失败`)
        }
        continue
      }

      let dispatchAttempt = 0
      const dispatch = await (async () => {
        while (true) {
          try {
            return await dispatchNextPriceTrackLoopRun(loop.id)
          } catch (error) {
            const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
            const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
            dispatchAttempt += 1
            if (!transient || dispatchAttempt >= 10) throw error
            queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后继续派发（${dispatchAttempt}/10）...`
            await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
          }
        }
      })()
      setLoopRun(dispatch.loopRun || loop)
      if (!dispatch.childTaskRequest) {
        clearLoopRunIfTerminal()
        break
      }
      const childTaskRequest = dispatch.childTaskRequest
      const row = childTaskRequest.items?.[0]
      if (!row) throw new Error('循环任务缺少待执行店铺')
      let createAttempt = 0
      const taskVo = await (async () => {
        while (true) {
          try {
            return await createPriceTrackTask(childTaskRequest)
          } catch (error) {
            const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
            const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
            createAttempt += 1
            if (!transient || createAttempt >= 10) throw error
            queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后重试创建子任务（${createAttempt}/10）...`
            await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
          }
        }
      })()
      recordCreatedTask(taskVo)
      const queuePayload = buildQueuePayload(taskVo, row)
      queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
      const pushResult = await api.enqueue_json(queuePayload)
      if (!pushResult?.success) {
        throw new Error(pushResult?.error || `任务 ${taskVo.taskId} 推送失败`)
      }
      addPollingTask(taskVo.taskId)
      scheduleNextPoll(true)
      removeMatchedRowsLocally([row])
      queuePushResult.value = `循环任务 ${loop.id} 第 ${dispatch.childTaskRequest.roundIndex || 1} 轮已推送：${row.shopName || taskVo.taskId}`
    }
  } finally {
    loopDispatching.value = false
  }
}

async function pushToPythonLoopQueue() {
  if (hasRunningLoop.value) {
    try {
      await runLoopExecution(activeLoopRunId.value || undefined)
    } catch (error) {
      if (error instanceof Error && /子任务不存在|循环任务不存在|404/.test(error.message)) {
        activeLoopRun.value = null
        activeLoopRunId.value = null
        saveActiveLoopRunId()
      }
      const message = error instanceof Error ? error.message : '循环执行失败'
      queuePushResult.value = message
      ElMessage.error(message)
    }
    return
  }
  if (!hasValidMode.value) {
    ElMessage.warning('请至少选择一个跟价模式')
    return
  }
  if (asinModeEnabled.value && !asinFiles.value.length) {
    ElMessage.warning('请先选择 ASIN 文件')
    return
  }
  const matchedRows = matchedItems.value.filter((item) => item.matched)
  if (!matchedRows.length) {
    ElMessage.warning('请先匹配可用店铺')
    return
  }
  pushing.value = true
  queuePayloadText.value = ''
  try {
    const loop = await createPriceTrackLoopRun({
      userId: 0,
      statusMode: statusModeEnabled.value,
      asinMode: asinModeEnabled.value,
      items: matchedRows,
      asinFiles: resolveAsinRequestPaths(),
      countryCodes: [...orderedCountryCodes.value],
      executionMode: executionMode.value,
      targetRounds: executionMode.value === 'FINITE' ? Math.max(1, Number(roundCount.value) || 1) : undefined,
    })
    setLoopRun(loop)
    queuePushResult.value = `循环任务 ${loop.id} 已创建`
    await runLoopExecution(loop.id)
    clearLoopRunIfTerminal()
    if (!activeLoopRunId.value) {
      ElMessage.success('跟价循环已启动')
    }
  } catch (error) {
    const message = error instanceof Error ? error.message : '启动循环失败'
    queuePushResult.value = message
    ElMessage.error(message)
  } finally {
    pushing.value = false
  }
}

function getPollIntervalMs() {
  return getTaskPollIntervalMs()
}

async function refreshTaskBatch() {
  const ids = pollingTaskIds.value.filter((id) => id > 0)
  if (!ids.length) return
  try {
    const batch = await getPriceTrackTaskProgressBatch(ids)
    let changed = false
    const nextSnapshots = { ...taskSnapshots.value }
    for (const missingId of batch.missingTaskIds || []) {
      removePollingTask(missingId)
      delete nextSnapshots[missingId]
    }
    for (const detail of batch.items || []) {
      const taskId = detail.task?.id
      const status = detail.task?.status
      if (typeof taskId !== 'number' || taskId <= 0) continue
      const prev = nextSnapshots[taskId]
      nextSnapshots[taskId] = prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail
      if (status) {
        taskDetails.value[taskId] = status
        if (status === 'SUCCESS' || status === 'FAILED') {
          removePollingTask(taskId)
          changed = true
        }
      }
    }
    taskSnapshots.value = nextSnapshots
    saveTaskSnapshotsToStorage()
    saveTaskDetailsToStorage()
    await loadHistory()
    syncPollingIdsWithHistory()
    if (changed) await loadDashboard()
  } catch {
    /* polling noise */
  }
}

function scheduleNextPoll(immediate = false) {
  if (pollTimer.value) {
    if (!immediate) return
    window.clearTimeout(pollTimer.value)
    pollTimer.value = null
  }
  const run = async () => {
    pollTimer.value = null
    if (pollingInFlight.value) { scheduleNextPoll(); return }
    if (!pollingTaskIds.value.length) return
    pollingInFlight.value = true
    try { await refreshTaskBatch() } finally { pollingInFlight.value = false }
    if (pollingTaskIds.value.length > 0) pollTimer.value = window.setTimeout(run, getPollIntervalMs())
  }
  if (immediate) void run()
  else pollTimer.value = window.setTimeout(run, getPollIntervalMs())
}

function stopPolling() {
  if (pollTimer.value) { window.clearTimeout(pollTimer.value); pollTimer.value = null }
}

function addPollingTask(taskId: number) {
  if (!pollingTaskIds.value.includes(taskId)) {
    pollingTaskIds.value = [...pollingTaskIds.value, taskId]
    savePollingIds()
  }
}

function removePollingTask(taskId: number) {
  pollingTaskIds.value = pollingTaskIds.value.filter((id) => id !== taskId)
  savePollingIds()
  delete taskDetails.value[taskId]
  saveTaskDetailsToStorage()
  if (taskSnapshots.value[taskId]) {
    const next = { ...taskSnapshots.value }
    delete next[taskId]
    taskSnapshots.value = next
    saveTaskSnapshotsToStorage()
  }
}

const currentSectionItems = computed(() => {
  const out = historyItems.value.filter((row) => row.taskId && pollingTaskIds.value.includes(row.taskId) && !isTaskTerminalById(row.taskId))
  const existingTaskIds = new Set(out.map((row) => row.taskId).filter((id): id is number => typeof id === 'number'))
  for (const taskId of pollingTaskIds.value) {
    if (existingTaskIds.has(taskId)) continue
    const first = taskSnapshots.value[taskId]?.items?.[0]
    if (first) out.push(first)
  }
  return out
})

const historySectionItems = computed(() =>
  historyItems.value.filter((row) => !row.taskId || !pollingTaskIds.value.includes(row.taskId) || isTaskTerminalById(row.taskId))
)

const hasVisibleList = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0)

function taskStatusOf(taskId?: number) {
  if (!taskId) return ''
  return taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status || ''
}

function isTaskTerminalById(taskId?: number) {
  const s = taskStatusOf(taskId)
  return s === 'SUCCESS' || s === 'FAILED'
}

function resolvedTaskStatus(item: PriceTrackHistoryItem) {
  const tid = item.taskId
  if (!tid) return item.taskStatus || ''
  return taskStatusOf(tid) || item.taskStatus || ''
}

function statusText(item: PriceTrackHistoryItem) {
  const st = resolvedTaskStatus(item)
  if (st === 'SUCCESS') return '已完成'
  if (st === 'FAILED') return '失败'
  if (st === 'RUNNING') return '执行中'
  if (item.success) return '已完成'
  return '等待中'
}

function statusClass(item: PriceTrackHistoryItem) {
  const st = resolvedTaskStatus(item)
  if (st === 'SUCCESS' || item.success) return 'success'
  if (st === 'FAILED') return 'failed'
  return 'running'
}

function canDownload(item: PriceTrackHistoryItem) {
  if (!item.resultId) return false
  const st = resolvedTaskStatus(item)
  return st === 'SUCCESS' || item.success === true
}

async function downloadResult(item: PriceTrackHistoryItem) {
  if (!item.resultId) return
  const api = getPywebviewApi()
  const filename = item.outputFilename || `${item.shopName || 'result'}.xlsx`
  if (!api?.save_file_from_url_new) {
    ElMessage.error('当前客户端未提供 save_file_from_url_new，无法下载文件')
    return
  }
  const url = getPriceTrackResultDownloadUrl(item.resultId)
  const result = await api.save_file_from_url_new(url, filename)
  if (result.success) {
    ElMessage.success(`已保存：${result.path || filename}`)
  } else if (result.error && result.error !== '用户取消') {
    ElMessage.error(result.error)
  }
}

async function deleteTaskRecord(item: PriceTrackHistoryItem) {
  try {
    if (item.resultId != null) {
      await deletePriceTrackHistory(item.resultId)
    } else if (item.taskId != null) {
      await deletePriceTrackTask(item.taskId)
    }
    if (activeLoopRun.value && ((item.loopRunId != null && item.loopRunId === activeLoopRun.value.id) || (item.taskId != null && item.taskId === activeLoopRun.value.activeTaskId))) {
      await syncActiveLoopRun()
    }
    if (item.taskId != null) removePollingTask(item.taskId)
    removeMatchedRowsLocally([item])
    await loadHistory()
    await loadDashboard()
    syncPollingIdsWithHistory()
    ElMessage.success('已删除')
  } catch (e) {
    if (isRecordMissingError(e)) {
      if (item.taskId != null) removePollingTask(item.taskId)
      removeMatchedRowsLocally([item])
      removeHistoryItemLocally(item)
      await loadDashboard()
      syncPollingIdsWithHistory()
      ElMessage.success('后端记录已不存在，已同步移除本地记录')
      return
    }
    ElMessage.error(e instanceof Error ? e.message : '删除失败')
  }
}

// ========== 生命周期 ==========
onMounted(() => {
  if (!statusModeEnabled.value && !asinModeEnabled.value) {
    selectMode('status')
  }
  loadPollingIdsFromStorage()
  loadTaskDetailsFromStorage()
  loadTaskSnapshotsFromStorage()
  loadMatchedItemsFromStorage()
  loadActiveLoopRunIdFromStorage()
  loadCandidates().catch(() => undefined)
  loadCountryPreference().catch(() => undefined)
  loadHistory().catch(() => undefined)
  loadDashboard().catch(() => undefined)
  if (pollingTaskIds.value.length) scheduleNextPoll(true)
  if (activeLoopRunId.value) {
    void syncActiveLoopRun().then((loop) => {
      if (loop?.activeTaskId) {
        addPollingTask(loop.activeTaskId)
        scheduleNextPoll(true)
      }
      if (loop && loop.status !== 'SUCCESS' && loop.status !== 'FAILED' && loop.status !== 'STOPPED') {
        void runLoopExecution(loop.id)
      }
    }).catch(() => {
      activeLoopRunId.value = null
      saveActiveLoopRunId()
    })
  }
})

onUnmounted(() => {
  stopPolling()
  if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer)
})
</script>

<style scoped>
.module-page {
  min-height: 100vh;
  background: #1a1a1a;
}

.main-content {
  display: flex;
  min-height: calc(100vh - 56px);
  height: calc(100vh - 56px);
}

.left-panel {
  width: 400px;
  background: #1e1e1e;
  padding: 20px;
  overflow-y: auto;
  border-right: 1px solid #2a2a2a;
}

.right-panel {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: #1a1a1a;
}

.section-title {
  font-size: 13px;
  color: #bbb;
  margin-bottom: 10px;
}

.upload-zone {
  border: 1px dashed #3a3a3a;
  border-radius: 10px;
  padding: 16px;
  background: #252525;
  margin-bottom: 20px;
}

.hint {
  color: #888;
  font-size: 12px;
  margin-bottom: 12px;
  line-height: 1.5;
  text-align: left;
}

.btns {
  display: flex;
  gap: 10px;
  margin-bottom: 10px;
}

.selected-files {
  margin-top: 8px;
  font-size: 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.clean-placeholder {
  color: #666;
  font-size: 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.selected-files span:not(.more-line) {
  color: #aaa;
}

.more-line {
  color: #777;
  font-style: italic;
}

.text-muted {
  color: #777;
  font-style: italic;
}

.mode-zone {
  border: 1px dashed #3a3a3a;
  border-radius: 10px;
  padding: 14px 16px;
  background: #252525;
  margin-bottom: 20px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.mode-check-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
  user-select: none;
  font-size: 13px;
}

.mode-check-input {
  width: 18px;
  height: 18px;
  accent-color: #409eff;
  cursor: pointer;
  flex-shrink: 0;
  margin-top: 2px;
}

.mode-check-text {
  color: #ddd;
  font-weight: 600;
}

.mode-hint-inline {
  color: #888;
  font-size: 12px;
}

.loop-round-input {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 4px;
}

.loop-round-input :deep(.el-input-number) {
  width: 160px;
}

.loop-round-input :deep(.el-input__wrapper) {
  background-color: #2a2a2a;
  box-shadow: 0 0 0 1px #3a3a3a inset;
}

.tag-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 500;
  margin-left: 6px;
  vertical-align: middle;
  background: rgba(52, 152, 219, 0.2);
  color: #69b6ff;
}

.mode-status-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border-radius: 8px;
  background: rgba(46, 204, 113, 0.08);
  border: 1px solid rgba(46, 204, 113, 0.2);
  margin-bottom: 14px;
}

.mode-status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

.mode-status-dot.active {
  background: #27ae60;
  box-shadow: 0 0 6px rgba(46, 204, 113, 0.5);
}

.mode-status-text {
  font-size: 12px;
  color: #aaa;
}

.input-zone {
  border: 1px dashed #3a3a3a;
  border-radius: 10px;
  padding: 16px;
  background: #252525;
  margin-bottom: 20px;
}

.input-row {
  display: flex;
  gap: 10px;
  align-items: center;
}

.input-row :deep(.el-input) {
  flex: 1;
}

.opt-btn {
  padding: 8px 14px;
  font-size: 13px;
  color: #ccc;
  background: #2a2a2a;
  border: 1px solid #3a3a3a;
  border-radius: 6px;
  cursor: pointer;
  white-space: nowrap;
}

.opt-btn:hover:not(:disabled) {
  color: #3498db;
  border-color: #3498db;
}

.opt-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.country-pref-hint {
  margin-top: -4px;
}

.country-pref-checks {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 16px;
  margin-bottom: 12px;
}

.country-check-row {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
  font-size: 12px;
  color: #ccc;
}

.country-check-input {
  width: 16px;
  height: 16px;
  accent-color: #409eff;
  cursor: pointer;
  flex-shrink: 0;
}

.country-check-text {
  line-height: 1.3;
}

.country-order-panel {
  border: 1px solid #333;
  border-radius: 8px;
  padding: 10px 12px;
  background: #252525;
  margin-bottom: 14px;
}

.country-order-caption {
  font-size: 11px;
  color: #777;
  margin-bottom: 8px;
}

.country-order-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.country-drag-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  background: #2c2c2c;
  border-radius: 6px;
  border: 1px solid #3a3a3a;
  cursor: grab;
  user-select: none;
}

.country-drag-row.dragging {
  opacity: 0.55;
  border-color: #3498db;
}

.drag-handle {
  color: #666;
  font-size: 12px;
  letter-spacing: -2px;
}

.country-drag-label {
  font-size: 12px;
  color: #ccc;
}

.country-pref-status {
  font-size: 11px;
  color: #888;
  margin-bottom: 10px;
}

.listing-filter-hint {
  margin-top: -4px;
}

.listing-filter-row {
  margin-bottom: 16px;
}

.listing-filter-select {
  width: 100%;
}

.listing-filter-row :deep(.el-select__wrapper) {
  background-color: #2a2a2a;
  box-shadow: 0 0 0 1px #3a3a3a inset;
}

.listing-filter-row :deep(.el-select__placeholder),
.listing-filter-row :deep(.el-select__selected-item) {
  color: #ccc;
}








.empty-candidates {
  color: #666;
  font-size: 13px;
  padding: 16px;
  border: 1px dashed #333;
  border-radius: 8px;
  margin-bottom: 16px;
}

.candidate-table-scroll {
  max-height: 240px;
  overflow: auto;
  margin-bottom: 16px;
  border-radius: 8px;
  border: 1px solid #2a2a2a;
}

.candidate-table {
  --el-table-bg-color: #252525;
  --el-table-tr-bg-color: #252525;
  --el-table-header-bg-color: #2a2a2a;
  --el-table-text-color: #ccc;
  --el-table-border-color: #333;
}

.link-danger {
  background: none;
  border: none;
  color: #e74c3c;
  cursor: pointer;
  font-size: 12px;
  padding: 0;
}

.link-danger:hover {
  text-decoration: underline;
}

.run-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 8px;
}

.btn-run {
  padding: 10px 18px;
  font-size: 14px;
  font-weight: 600;
  color: #fff;
  background: #3498db;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}

.btn-run:hover:not(:disabled) {
  background: #2980b9;
}

.btn-run:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

.btn-queue {
  background: #27ae60;
}

.btn-queue:hover:not(:disabled) {
  background: #219a52;
}

.loading-msg {
  margin-top: 12px;
  font-size: 12px;
  color: #777;
  line-height: 1.5;
}

.loop-status-card {
  margin-top: 14px;
  padding: 12px 14px;
  border-radius: 10px;
  border: 1px solid #2f3d2f;
  background: rgba(39, 174, 96, 0.08);
}

.loop-status-line {
  font-size: 12px;
  color: #d6e6d8;
  line-height: 1.6;
}

.loop-error {
  color: #ff8a80;
}

.loop-stop-btn {
  margin-top: 10px;
}

.loading-msg code {
  font-size: 11px;
  color: #8fd3ff;
}

.queue-debug-card {
  margin-top: 18px;
  padding: 14px;
  border: 1px solid #2a2a2a;
  border-radius: 10px;
  background: #202020;
}

.queue-debug-title {
  margin-bottom: 8px;
}

.queue-debug-line {
  color: #b8c1cc;
  font-size: 12px;
  line-height: 1.6;
  margin-bottom: 8px;
}

.queue-debug-payload {
  margin: 0;
  max-height: 220px;
  overflow: auto;
  padding: 10px;
  border-radius: 8px;
  background: #141414;
  color: #8fd3ff;
  font-size: 11px;
  line-height: 1.5;
  white-space: pre-wrap;
  word-break: break-all;
}

/* Right panel */
.panel-header {
  padding: 16px 20px;
  font-size: 15px;
  font-weight: 600;
  color: #e0e0e0;
  border-bottom: 1px solid #2a2a2a;
}

.task-list-wrap {
  flex: 1;
  padding: 16px 20px;
  overflow: auto;
}

.pr-summary {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  margin-bottom: 16px;
}

.summary-card {
  padding: 14px 16px;
  border: 1px solid #2a2a2a;
  border-radius: 10px;
  background: #1e1e1e;
}

.summary-card strong {
  display: block;
  margin-top: 8px;
  font-size: 22px;
  color: #eaf4ff;
}

.summary-label {
  font-size: 12px;
  color: #8d8d8d;
}

.subsection-title {
  font-size: 13px;
  color: #999;
  margin: 8px 0 10px;
}

.empty-tasks {
  color: #666;
  font-size: 13px;
  padding: 16px;
  text-align: center;
}

.empty-tasks.narrow {
  padding: 12px 8px;
}

.match-table {
  margin-bottom: 18px;
}

.result-table {
  --el-table-bg-color: #222;
  --el-table-tr-bg-color: #222;
  --el-table-header-bg-color: #2a2a2a;
  --el-table-text-color: #ccc;
  --el-table-border-color: #333;
}

.ok { color: #27ae60; }
.fail { color: #e67e22; }

.result-list-wrap {
  border: 1px solid #2a2a2a;
  border-radius: 10px;
  background: #1e1e1e;
  min-height: 200px;
  margin-top: 8px;
}

.result-list-header {
  padding: 12px 16px;
  border-bottom: 1px solid #2a2a2a;
  font-size: 14px;
  color: #ddd;
}

.result-subsection {
  padding: 12px 12px 4px;
}

.result-subsection-title {
  font-size: 12px;
  color: #888;
  margin-bottom: 8px;
}

.task-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.task-item {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 14px;
  border: 1px solid #2a2a2a;
  border-radius: 8px;
  margin-bottom: 8px;
  background: #222;
}

.split-result-main {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.id {
  font-weight: 600;
  color: #e0e0e0;
  font-size: 13px;
}

.files {
  font-size: 12px;
  color: #888;
}

.task-right {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.status {
  padding: 4px 10px;
  border-radius: 6px;
  font-size: 12px;
  white-space: nowrap;
}

.status.success {
  background: rgba(46, 204, 113, 0.18);
  color: #2ecc71;
}

.status.failed {
  background: rgba(231, 76, 60, 0.18);
  color: #ff6b6b;
}

.status.running {
  background: rgba(52, 152, 219, 0.18);
  color: #3498db;
}

.download {
  padding: 6px 10px;
  border-radius: 6px;
  font-size: 12px;
  background: rgba(52, 152, 219, 0.18);
  color: #69b6ff;
  border: none;
  cursor: pointer;
}

.download:hover {
  background: rgba(52, 152, 219, 0.28);
}

.btn-delete {
  padding: 6px 10px;
  border-radius: 6px;
  font-size: 12px;
  background: rgba(231, 76, 60, 0.12);
  color: #ff8f8f;
  border: none;
  cursor: pointer;
}

.btn-delete:hover {
  background: rgba(231, 76, 60, 0.22);
}

@media (max-width: 1100px) {
  .main-content {
    flex-direction: column;
    height: auto;
  }

  .left-panel {
    width: 100%;
    border-right: none;
    border-bottom: 1px solid #2a2a2a;
  }

  .pr-summary {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
</style>

