<template>
  <div class="page-shell module-page">
    <BrandTopBar active="product-risk" />

    <div class="main-content">
      <aside class="left-panel">
        <div class="section-title">店铺名称</div>
        <div class="input-zone">
          <div class="hint">输入店铺名后点「确认」加入下方备选区（按当前登录用户保存）。仅索引可命中的店铺可入库。勾选备选后点「匹配店铺」，再「推送到 Python 队列」。</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="240" class="candidate-table"
            @selection-change="onSelectionChange">
            <el-table-column type="selection" width="42" />
            <el-table-column prop="shop_name" 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>

        <div class="section-title">处理国家与顺序</div>
        <p class="hint country-pref-hint">
          勾选需要处理的站点（可只选 2～3 国）；上方勾选区<strong>先按已选顺序</strong>排列，其余未选国家排在后面。拖拽下方列表调整顺序后会与上方同步。设置会按当前登录用户保存。
        </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)"
              @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 class="section-title">商品列表筛选</div>
        <p class="hint listing-filter-hint">与「推送到 Python 队列」一并下发，供 Python 区分处理场景。</p>
        <div class="listing-filter-row">
          <el-select v-model="productRiskListingFilter" class="listing-filter-select" teleported placeholder="选择筛选类型">
            <el-option v-for="opt in PRODUCT_RISK_LISTING_FILTER_OPTIONS" :key="opt.value" :label="opt.label"
              :value="opt.value" />
          </el-select>
        </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 || !matchedItems.length"
            @click="pushToPythonQueue">
            {{ pushing ? '推送中…' : '推送到 Python 队列' }}
          </button>
        </div>
        <p class="loading-msg">与删除 ASIN 品牌相同：先创建任务，再按店铺<strong>逐条</strong>入队（每条 <code>data.items</code>
          仅含一个店铺）。队列串行执行；Python
          每处理完一店可 POST <code>/api/product-risk-resolve/tasks/{taskId}/result</code>（可多次，每次可只含已完成的店铺）；全部完成后任务结束。前端轮询任务状态。
        </p>

        <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">
                      <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">
                      <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 {
  addProductRiskCandidate,
  createProductRiskTask,
  deletePendingProductRiskShopResult,
  deleteProductRiskCandidate,
  deleteProductRiskHistory,
  deleteProductRiskTask,
  getProductRiskCountryPreference,
  getProductRiskDashboard,
  getProductRiskHistory,
  getProductRiskResultDownloadUrl,
  getProductRiskTasksBatch,
  listProductRiskCandidates,
  matchProductRiskShops,
  putProductRiskCountryPreference,
  type ProductRiskCandidateVo,
  type ProductRiskDashboardVo,
  type ProductRiskHistoryItem,
  type ProductRiskShopQueueItem,
  type ProductRiskTaskDetailVo,
} from '@/shared/api/java-modules'
import { getPywebviewApi } from '@/shared/bridges/pywebview'

const shopInput = ref('')
const candidates = ref<ProductRiskCandidateVo[]>([])
const selectedCandidates = ref<ProductRiskCandidateVo[]>([])
const matchedItems = ref<ProductRiskShopQueueItem[]>([])
const adding = ref(false)
const matching = ref(false)
const pushing = ref(false)
const queuePushResult = ref('')
const queuePayloadText = ref('')

/** 与后端 ProductRiskCountryCode / 默认顺序 DE→UK→FR→IT→ES 一致 */
const COUNTRY_OPTIONS = [
  { code: 'DE', label: '德国' },
  { code: 'UK', label: '英国' },
  { code: 'FR', label: '法国' },
  { code: 'IT', label: '意大利' },
  { code: 'ES', label: '西班牙' },
] as const

/** 与 Python 约定：队列 payload.data.risk_listing_filter */
const PRODUCT_RISK_LISTING_FILTER_OPTIONS = [
  { value: 'SearchSuppressed', label: '在搜索结果中禁止显示' },
  { value: 'ApprovalRequired', label: '需要批准' },
  { value: 'Active', label: '在售' },
] as const

type ProductRiskListingFilter = (typeof PRODUCT_RISK_LISTING_FILTER_OPTIONS)[number]['value']

const orderedCountryCodes = ref<string[]>(['DE', 'UK', 'FR', 'IT', 'ES'])
const productRiskListingFilter = ref<ProductRiskListingFilter>('SearchSuppressed')
const dragCountryIndex = ref<number | null>(null)
const countryPrefSaving = ref(false)
/** 用户已改过顺序/勾选后，忽略晚到的 GET，避免把界面打回全选 */
const countryPrefUserTouched = ref(false)
let countryPrefSaveTimer: ReturnType<typeof setTimeout> | null = null

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

/** 已选国家按当前顺序在前，未选按默认 DE→UK→FR→IT→ES 在后，与下方拖拽列表一致 */
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 onCountryNativeChange(code: string, e: Event) {
  const el = e.target as HTMLInputElement | null
  if (!el) 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 putProductRiskCountryPreference(orderedCountryCodes.value)
  } catch (e) {
    ElMessage.error(e instanceof Error ? e.message : '保存国家顺序失败')
  } finally {
    countryPrefSaving.value = false
  }
}

async function loadCountryPreference() {
  try {
    const vo = await getProductRiskCountryPreference()
    if (countryPrefUserTouched.value) {
      return
    }
    if (vo?.country_codes?.length) {
      orderedCountryCodes.value = [...vo.country_codes]
    }
  } catch {
    /* 未登录或接口失败时保留本地默认 */
  }
}

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

const historyItems = ref<ProductRiskHistoryItem[]>([])
const pollingTaskIds = ref<number[]>([])
const taskDetails = ref<Record<number, string>>({})
/** 本地缓存 POST /tasks/batch 各 task 最新快照 + 创建任务响应，便于刷新后查看 */
const taskSnapshots = ref<Record<number, ProductRiskTaskDetailVo>>({})
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 `product-risk:tasks:${uidForStorage()}`
}

function taskStatusStorageKey() {
  return `product-risk:task-status:${uidForStorage()}`
}

function taskSnapshotsStorageKey() {
  return `product-risk:tasks-batch-snapshot:${uidForStorage()}`
}

function matchedItemsStorageKey() {
  return `product-risk:matched-items:${uidForStorage()}`
}

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

function saveTaskDetailsToStorage() {
  if (typeof window === 'undefined') return
  try {
    window.localStorage.setItem(taskStatusStorageKey(), JSON.stringify(taskDetails.value))
  } catch {
    /* quota */
  }
}

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

function saveTaskSnapshotsToStorage() {
  if (typeof window === 'undefined') return
  try {
    window.localStorage.setItem(taskSnapshotsStorageKey(), JSON.stringify(taskSnapshots.value))
  } catch {
    /* quota */
  }
}

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 ProductRiskShopQueueItem[]
  } catch {
    /* ignore */
  }
}

function saveMatchedItemsToStorage() {
  if (typeof window === 'undefined') return
  try {
    window.localStorage.setItem(matchedItemsStorageKey(), JSON.stringify(matchedItems.value))
  } catch {
    /* quota */
  }
}

function rowKeyForMatch(row: ProductRiskShopQueueItem) {
  const name = (row.shopName || '').trim()
  const id = row.shopId ?? ''
  return `${name}\u0001${id}`
}

async function removeMatchedRow(row: ProductRiskShopQueueItem) {
  const key = rowKeyForMatch(row)
  const backup = [...matchedItems.value]
  matchedItems.value = matchedItems.value.filter((r) => rowKeyForMatch(r) !== key)
  saveMatchedItemsToStorage()
  const name = (row.shopName || '').trim()
  if (!name) {
    ElMessage.success('已从匹配结果中移除')
    return
  }
  try {
    const res = await deletePendingProductRiskShopResult(name)
    if (res?.removed) {
      await loadHistory()
      await loadDashboard()
      syncPollingIdsWithHistory()
    }
    ElMessage.success('已从匹配结果中移除')
  } catch (e) {
    matchedItems.value = backup
    saveMatchedItemsToStorage()
    ElMessage.error(e instanceof Error ? e.message : '同步后端失败')
  }
}

/** 历史列表中已无该 taskId 时，停止轮询并清本地快照 */
function syncPollingIdsWithHistory() {
  for (const taskId of [...pollingTaskIds.value]) {
    const any = historyItems.value.some((r) => r.taskId === taskId)
    if (!any) {
      removePollingTask(taskId)
    }
  }
}

async function deleteTaskRecord(item: ProductRiskHistoryItem) {
  try {
    if (item.resultId != null && item.resultId > 0) {
      await deleteProductRiskHistory(item.resultId)
    } else if (item.taskId != null && item.taskId > 0) {
      await deleteProductRiskTask(item.taskId)
    } else {
      ElMessage.warning('无法删除：缺少记录标识')
      return
    }
    await loadHistory()
    await loadDashboard()
    syncPollingIdsWithHistory()
    ElMessage.success('已删除')
  } catch (e) {
    ElMessage.error(e instanceof Error ? e.message : '删除失败')
  }
}

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') : []
  } catch {
    pollingTaskIds.value = []
  }
}

function savePollingIds() {
  if (typeof window !== 'undefined') {
    window.localStorage.setItem(sessionKey(), JSON.stringify(pollingTaskIds.value))
  }
}

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()
  }
}

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

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

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

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

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

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

const currentSectionItems = computed(() =>
  historyItems.value.filter(
    (row) =>
      row.taskId &&
      pollingTaskIds.value.includes(row.taskId) &&
      !isTaskTerminalById(row.taskId),
  ),
)

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)

async function refreshTaskBatch() {
  const ids = pollingTaskIds.value.filter((id) => id > 0)
  if (!ids.length) return
  try {
    const batch = await getProductRiskTasksBatch(ids)
    let changed = false
    const nextSnapshots = { ...taskSnapshots.value }
    for (const d of batch.items || []) {
      const id = d.task?.id
      const st = d.task?.status
      if (typeof id === 'number' && id > 0) {
        nextSnapshots[id] = d
        if (st) {
          taskDetails.value[id] = st
          if (st === 'SUCCESS' || st === 'FAILED') {
            removePollingTask(id)
            changed = true
          }
        }
      }
    }
    taskSnapshots.value = nextSnapshots
    saveTaskSnapshotsToStorage()
    saveTaskDetailsToStorage()
    if (changed) {
      await loadHistory()
      await loadDashboard()
    }
  } catch {
    /* polling noise */
  }
}

function getPollIntervalMs() {
  return document.visibilityState === 'visible' ? 4500 : 10000
}

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 ensurePolling(immediate = false) {
  if (pollTimer.value && !immediate) return
  scheduleNextPoll(immediate)
}

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

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

async function removeCandidate(row: ProductRiskCandidateVo) {
  try {
    await deleteProductRiskCandidate(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?.shop_name?.trim()) set.add(r.shop_name.trim())
  }
  return [...set]
}

/** 多次「匹配店铺」时合并结果：已展示的店铺保留顺序，新店铺追加；同名店铺用本次接口结果覆盖 */
function mergeMatchedItems(
  existing: ProductRiskShopQueueItem[],
  incoming: ProductRiskShopQueueItem[],
): ProductRiskShopQueueItem[] {
  const incomingByName = new Map<string, ProductRiskShopQueueItem>()
  for (const item of incoming) {
    const k = (item.shopName || '').trim()
    if (k) incomingByName.set(k, item)
  }

  const usedIncoming = new Set<string>()
  const out: ProductRiskShopQueueItem[] = []

  for (const item of existing) {
    const k = (item.shopName || '').trim()
    if (k && incomingByName.has(k)) {
      out.push(incomingByName.get(k)!)
      usedIncoming.add(k)
    } else {
      out.push(item)
    }
  }
  for (const item of incoming) {
    const k = (item.shopName || '').trim()
    if (!k) {
      out.push(item)
      continue
    }
    if (!usedIncoming.has(k)) {
      out.push(item)
      usedIncoming.add(k)
    }
  }
  return out
}

async function runMatch() {
  const names = collectShopNamesForMatch()
  if (!names.length) {
    ElMessage.warning('请勾选备选店铺，或在输入框中填写店名（可与勾选合并）')
    return
  }
  matching.value = true
  try {
    const res = await matchProductRiskShops(names)
    const batch = res.items || []
    matchedItems.value = mergeMatchedItems(matchedItems.value, batch)
    saveMatchedItemsToStorage()
    if (!batch.length) {
      ElMessage.warning('未返回匹配结果')
    } else {
      ElMessage.success(`本次匹配 ${batch.length} 个店铺名，表格共 ${matchedItems.value.length} 条`)
    }
  } catch (e) {
    ElMessage.error(e instanceof Error ? e.message : '匹配失败')
  } finally {
    matching.value = false
  }
}

function statusText(item: ProductRiskHistoryItem) {
  const tid = item.taskId
  const st = tid ? taskStatusOf(tid) : item.taskStatus
  if (st === 'SUCCESS') return '已完成'
  if (st === 'FAILED') return '失败'
  if (st === 'RUNNING') return '执行中'
  if (item.success) return '已完成'
  return '等待中'
}

function statusClass(item: ProductRiskHistoryItem) {
  const tid = item.taskId
  const st = tid ? taskStatusOf(tid) : item.taskStatus
  if (st === 'SUCCESS' || item.success) return 'success'
  if (st === 'FAILED') return 'failed'
  return 'running'
}

function canDownload(item: ProductRiskHistoryItem) {
  if (!item.resultId) return false
  const tid = item.taskId
  const st = tid ? taskStatusOf(tid) : item.taskStatus
  return st === 'SUCCESS' || item.success === true
}

async function downloadResult(item: ProductRiskHistoryItem) {
  if (!item.resultId) return
  const api = getPywebviewApi()
  const url = getProductRiskResultDownloadUrl(item.resultId)
  const filename = item.outputFilename || `${item.shopName || 'result'}.zip`
  if (api?.save_file_from_url_new) {
    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)
    }
    return
  }
  window.open(url, '_blank')
}

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: ProductRiskShopQueueItem) {
  const msg = (row.matchMessage || '').trim()
  if (msg) {
    return msg
  }
  if (row.matched && row.matchStatus === 'MATCHED') {
    return '紫鸟索引已命中，可推送队列；打开店铺地址由 Python 侧处理'
  }
  if (row.matched) {
    return '已关联索引，请结合状态列查看是否可推送'
  }
  return '未命中或未就绪，请检查店铺名与索引刷新'
}

async function pushToPythonQueue() {
  if (!matchedItems.value.length) {
    ElMessage.warning('请先匹配店铺')
    return
  }
  const api = getPywebviewApi()
  if (!api?.enqueue_json) {
    ElMessage.error('当前环境未启用 pywebview enqueue_json（浏览器内无法推队列）')
    return
  }
  const toPush = matchedItems.value.filter((i) => i.matched)
  if (!toPush.length) {
    ElMessage.warning('没有已匹配的店铺可推送')
    return
  }
  pushing.value = true
  queuePayloadText.value = ''
  let createdTaskId: number | null = null
  try {
    const created = await createProductRiskTask(toPush)
    createdTaskId = created.taskId
    taskSnapshots.value = {
      ...taskSnapshots.value,
      [created.taskId]: {
        task: { id: created.taskId, status: 'RUNNING' },
        items: created.items,
      },
    }
    saveTaskSnapshotsToStorage()
    addPollingTask(created.taskId)
    for (let i = 0; i < toPush.length; i++) {
      const item = toPush[i]
      const payload = {
        type: 'product-risk-resolve-run',
        ts: Date.now(),
        data: {
          taskId: created.taskId,
          items: [item],
          country_codes: [...orderedCountryCodes.value],
          risk_listing_filter: productRiskListingFilter.value,
        },
      }
      queuePayloadText.value = JSON.stringify(payload, null, 2)
      const pushResult = await api.enqueue_json(payload)
      if (!pushResult?.success) {
        removePollingTask(created.taskId)
        queuePushResult.value = `第 ${i + 1}/${toPush.length} 条推送失败：${pushResult?.error || '未知错误'}`
        ElMessage.error(queuePushResult.value)
        return
      }
      queuePushResult.value = `任务 ${created.taskId}：已入队 ${i + 1}/${toPush.length} 条店铺，当前队列长度：${pushResult.queue_size ?? '-'}`
    }
    ElMessage.success(`已创建任务 ${created.taskId}，${toPush.length} 条店铺已逐条入队`)
    await loadHistory()
    ensurePolling(true)
  } catch (e) {
    if (createdTaskId != null) removePollingTask(createdTaskId)
    queuePushResult.value = e instanceof Error ? e.message : '推送异常'
    ElMessage.error(queuePushResult.value)
  } finally {
    pushing.value = false
  }
}

onMounted(() => {
  loadPollingIdsFromStorage()
  loadTaskDetailsFromStorage()
  loadTaskSnapshotsFromStorage()
  loadMatchedItemsFromStorage()
  loadCandidates().catch(() => undefined)
  loadCountryPreference().catch(() => undefined)
  loadHistory()
    .then(() => {
      syncPollingIdsWithHistory()
      return refreshTaskBatch()
    })
    .catch(() => undefined)
  loadDashboard().catch(() => undefined)
  if (pollingTaskIds.value.length) ensurePolling(true)
})

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;
}

.input-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;
}

.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;
}

.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;
}

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

.candidate-table-scroll {
  max-height: 280px;
  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;
}

.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;
}

.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;
}

.clean-result-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;
}

/* 与主题统一：不要斑马纹、不要悬停/选中行变亮底 */
.match-table :deep(.el-table__body tr:hover > td.el-table__cell),
.match-table :deep(.el-table__body tr.hover-row > td.el-table__cell),
.match-table :deep(.el-table__body tr.current-row > td.el-table__cell) {
  background-color: var(--el-table-tr-bg-color, #222) !important;
}

.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;
}

.left {
  flex: 1;
  min-width: 0;
}

.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;
  }

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