<template>
  <div
    @dragenter.prevent="handleDragEnter"
    @dragover.prevent="handleDragOver"
    @dragleave="handleDragLeave"
    @drop.prevent="handleDrop"
  >
    <div v-if="isServicesPortal" class="portal-bar">
      <span class="portal-bar-title">版本管理</span>
      <el-select :model-value="appKey" placeholder="选择应用" style="width:220px" @change="switchApp">
        <el-option v-for="a in portalApps" :key="a.appKey" :label="a.name" :value="a.appKey" />
      </el-select>
    </div>
    <el-page-header v-else @back="$router.back()" :content="`版本管理 — ${pageTitle}`" style="margin-bottom:20px" />
    <el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />

    <div v-if="isServicesPortal && appKey && checkingService" v-loading="true" style="min-height:200px" />

    <template v-if="isServicesPortal && appKey && serviceEnabled === false">
      <el-empty :image-size="80" description="当前应用未开通版本管理服务" style="margin-top:60px">
        <el-button type="primary" @click="showActivationDialog = true">申请开通</el-button>
      </el-empty>
      <el-dialog v-model="showActivationDialog" title="申请开通版本管理" width="460px">
        <el-form label-width="80px">
          <el-form-item label="服务">版本管理</el-form-item>
          <el-form-item label="申请理由">
            <el-input v-model="activationReason" type="textarea" :rows="3" placeholder="请描述您的业务场景" />
          </el-form-item>
        </el-form>
        <template #footer>
          <el-button @click="showActivationDialog = false">取消</el-button>
          <el-button type="primary" :loading="submittingActivation" @click="submitActivation">提交申请</el-button>
        </template>
      </el-dialog>
    </template>

    <template v-if="!isServicesPortal || (appKey && serviceEnabled === true)">
    <el-card>
      <el-tabs v-model="activeTab">
        <!-- App Versions -->
        <el-tab-pane label="App 整包版本" name="app">
          <div class="toolbar responsive-toolbar">
            <el-radio-group v-model="appPlatform" @change="loadAppVersions" style="margin-right:12px">
              <el-radio-button value="ANDROID">Android</el-radio-button>
              <el-radio-button value="IOS">iOS</el-radio-button>
              <el-radio-button value="HARMONY">Harmony</el-radio-button>
            </el-radio-group>
            <el-button type="primary" @click="openUploadAppDialog" :disabled="!platformPackageName">上传新版本</el-button>
            <el-button @click="loadAppVersions" :loading="loadingApp">刷新</el-button>
            <el-button @click="copyDownloadPageLink">复制对外下载页链接</el-button>
          </div>
          <el-alert
            v-if="!platformPackageName"
            :title="`${appPlatform === 'ANDROID' ? 'Android' : appPlatform === 'IOS' ? 'iOS' : '鸿蒙'}平台未配置包名，请先在应用信息中配置`"
            type="warning"
            show-icon
            :closable="false"
            style="margin-bottom:12px"
          />

          <div class="table-wrap">
          <el-table :data="appVersions" v-loading="loadingApp" border stripe>
            <el-table-column prop="versionName" label="版本名" width="110" />
            <el-table-column prop="versionCode" label="版本码" width="90" />
            <el-table-column label="状态" width="140">
              <template #default="{row}">
                <el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag>
                <el-tag v-if="row.scheduledPublishAt && row.publishStatus === 'DRAFT'" type="warning" size="small" style="margin-left:4px">
                  定时 {{ formatTime(row.scheduledPublishAt) }}
                </el-tag>
                <el-tag v-if="row.grayEnabled" type="warning" size="small" style="margin-left:4px">
                  灰度 {{ row.grayPercent }}%
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="应用商店" width="220" show-overflow-tooltip>
              <template #default="{row}">
                <div v-if="parseStoreReview(row.storeReviewStatus).length" class="store-review-cell">
                  <div class="store-review-tags">
                    <template v-for="item in parseStoreReview(row.storeReviewStatus)" :key="item.store">
                      <el-tag
                        :type="reviewTagType(item.state)"
                        size="small"
                        style="margin:2px"
                      >{{ storeLabel(item.store) }} · {{ reviewDisplayLabel(item) }}</el-tag>
                    </template>
                  </div>
                  <el-button
                    link
                    type="primary"
                    size="small"
                    class="store-review-detail-btn"
                    @click="openStoreReviewDetail(row)"
                  >查看详情</el-button>
                </div>
                <span v-else class="text-muted">—</span>
              </template>
            </el-table-column>
            <el-table-column prop="forceUpdate" label="强制" width="70">
              <template #default="{row}">
                <el-tag :type="row.forceUpdate ? 'danger' : 'info'" size="small">
                  {{ row.forceUpdate ? '是' : '否' }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="更新说明" min-width="160">
              <template #default="{row}">
                <el-tooltip :content="row.changeLog || '（无）'" placement="top" :disabled="!row.changeLog">
                  <span class="changelog-text">{{ row.changeLog || '—' }}</span>
                </el-tooltip>
                <el-button link type="primary" size="small" style="margin-left:4px" @click="openChangelogEdit(row)">
                  <el-icon><Edit /></el-icon>
                </el-button>
              </template>
            </el-table-column>
            <el-table-column prop="createdAt" label="上传时间" width="160">
              <template #default="{row}">{{ formatTime(row.createdAt) }}</template>
            </el-table-column>
            <el-table-column label="操作" width="280" fixed="right">
              <template #default="{row}">
                <el-button
                  v-if="row.publishStatus === 'DRAFT'"
                  link type="success" size="small"
                  @click="openPublishDialog(row, 'app')">发布</el-button>
                <el-button
                  v-if="row.publishStatus === 'DEPRECATED'"
                  link type="warning" size="small"
                  @click="openPublishDialog(row, 'app')">重新上架</el-button>
                <el-button
                  v-if="row.publishStatus === 'PUBLISHED' && !publishConfigForm.allowAnonymousUpdateCheck"
                  link type="warning" size="small"
                  @click="openGrayDialog(row, 'app')">灰度</el-button>
                <el-button
                  v-if="row.publishStatus === 'PUBLISHED'"
                  link type="primary" size="small"
                  @click="openPublishDialog(row, 'app')">修改强更</el-button>
                <el-button
                  v-if="row.publishStatus === 'PUBLISHED'"
                  link type="danger" size="small"
                  @click="promptUnpublishApp(row.id)">下架</el-button>
                <el-button
                  v-if="row.downloadUrl"
                  link type="primary" size="small"
                  tag="a" :href="row.downloadUrl" target="_blank">下载</el-button>
                <el-button
                  v-if="row.downloadUrl && row.publishStatus !== 'DEPRECATED'"
                  link type="primary" size="small"
                  @click="openSubmitStoreDialog(row)">提交市场</el-button>
                <el-button
                  v-if="row.publishStatus === 'DRAFT' && !hasActiveStoreReview(row)"
                  link type="danger" size="small"
                  @click="promptDeleteAppVersion(row)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          </div>
        </el-tab-pane>

        <!-- RN Bundles -->
        <el-tab-pane label="RN Bundle 热更新" name="rn">
          <div class="toolbar responsive-toolbar">
            <el-input
              v-model="rnModuleFilter"
              placeholder="模块ID（可选）"
              clearable
              style="width:180px;margin-right:8px"
              @change="loadRnBundles"
            />
            <el-radio-group v-model="rnPlatform" @change="loadRnBundles" style="margin-right:12px">
              <el-radio-button value="">全平台</el-radio-button>
              <el-radio-button value="ANDROID">Android</el-radio-button>
              <el-radio-button value="IOS">iOS</el-radio-button>
            </el-radio-group>
            <el-button type="primary" @click="showUploadRn = true">上传 Bundle</el-button>
            <el-button @click="loadRnBundles" :loading="loadingRn">刷新</el-button>
          </div>

          <div class="table-wrap">
          <el-table :data="rnBundles" v-loading="loadingRn" border stripe>
            <el-table-column prop="moduleId" label="模块ID" width="140" />
            <el-table-column prop="type" label="类型" width="90" />
            <el-table-column prop="version" label="版本" width="100" />
            <el-table-column prop="platform" label="平台" width="90" />
            <el-table-column prop="appVersionRange" label="整包版本范围" width="170" />
            <el-table-column prop="commonVersionRange" label="Common 版本范围" width="180">
              <template #default="{row}">{{ row.commonVersionRange || '—' }}</template>
            </el-table-column>
            <el-table-column prop="builtAgainstNativeBaselineId" label="原生基线" width="220" show-overflow-tooltip />
            <el-table-column prop="buildId" label="构建 ID" width="150" show-overflow-tooltip />
            <el-table-column prop="bundleFormat" label="Bundle 格式" width="140" />
            <el-table-column prop="minNativeApiLevel" label="最低原生 API" width="130" />
            <el-table-column prop="archiveSha256" label="归档 SHA-256" width="220" show-overflow-tooltip />
            <el-table-column label="状态" width="140">
              <template #default="{row}">
                <el-tag :type="statusTagType(row)" size="small">{{ statusLabel(row) }}</el-tag>
                <el-tag v-if="row.scheduledPublishAt && row.publishStatus === 'DRAFT'" type="warning" size="small" style="margin-left:4px">
                  定时 {{ formatTime(row.scheduledPublishAt) }}
                </el-tag>
                <el-tag v-if="row.grayEnabled" type="warning" size="small" style="margin-left:4px">
                  灰度 {{ row.grayPercent }}%
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="note" label="说明" show-overflow-tooltip />
            <el-table-column prop="createdAt" label="上传时间" width="160">
              <template #default="{row}">{{ formatTime(row.createdAt) }}</template>
            </el-table-column>
            <el-table-column label="操作" width="160" fixed="right">
              <template #default="{row}">
                <el-button v-if="row.publishStatus === 'DRAFT'" link type="success" size="small" @click="openPublishDialog(row, 'rn')">发布</el-button>
                <el-button v-if="row.publishStatus === 'DEPRECATED'" link type="warning" size="small" @click="openPublishDialog(row, 'rn')">重新上架</el-button>
                <el-button v-if="row.publishStatus === 'PUBLISHED' && !publishConfigForm.allowAnonymousUpdateCheck" link type="warning" size="small" @click="openGrayDialog(row, 'rn')">灰度</el-button>
                <el-button v-if="row.publishStatus === 'PUBLISHED'" link type="danger" size="small" @click="promptUnpublishRn(row.id)">下架</el-button>
              </template>
            </el-table-column>
          </el-table>
          </div>
        </el-tab-pane>

        <!-- App Store Config -->
        <el-tab-pane label="应用商店配置" name="store">
          <el-alert
            title="应用商店配置按渠道分别维护，App Store 和鸿蒙只填写跳转页，不再填写密钥。审核通知单独配置一次，所有市场共享。"
            type="info"
            show-icon
            :closable="false"
            style="margin-bottom:16px"
          />
          <el-tabs v-model="storeTab">
            <el-tab-pane label="凭据配置" name="configs">
              <div class="store-upload-options">
                <span class="store-option-label">并行上传</span>
                <el-switch
                  v-model="publishConfigForm.parallelStoreUpload"
                  :loading="savingPublishConfig"
                  @change="savePublishConfig"
                />
                <span class="store-option-hint">开启后同时向所有商店提交包，关闭后逐个排队上传</span>
              </div>
              <div class="store-grid">
                <el-card
                  v-for="store in STORE_DEFS"
                  :key="store.type"
                  class="store-card"
                  shadow="hover"
                >
                  <div class="store-card-header">
                    <span class="store-card-name">{{ store.label }}</span>
                    <el-switch
                      :model-value="isStoreEnabled(store.type)"
                      :disabled="!getStoreConfig(store.type)"
                      @change="(val: boolean) => toggleStore(store.type, val)"
                    />
                  </div>
                  <div class="store-card-status">
                    <el-tag v-if="getStoreConfig(store.type)" type="success" size="small">已配置</el-tag>
                    <el-tag v-else type="info" size="small">未配置</el-tag>
                  </div>
                  <div class="store-card-meta">
                    <div v-if="getStoreConfig(store.type)">更新于 {{ formatTime(getStoreConfig(store.type)?.updatedAt ?? '') }}</div>
                    <div v-else>请先补齐 {{ store.shortLabel }} 的配置</div>
                  </div>
                  <div class="store-card-footer">
                    <el-button size="small" @click="openStoreConfigDialog(store)">
                      {{ getStoreConfig(store.type) ? '编辑配置' : '配置' }}
                    </el-button>
                    <el-button
                      v-if="getStoreConfig(store.type)"
                      size="small" type="danger"
                      @click="removeStoreConfig(store.type)"
                    >删除</el-button>
                  </div>
                </el-card>
              </div>
            </el-tab-pane>
            <el-tab-pane label="应用配置指引" name="guide">
              <!-- 平台选择 -->
              <div class="guide-platform-tabs">
                <el-radio-group v-model="guidePlatform" size="default">
                  <el-radio-button value="android">🤖 Android</el-radio-button>
                  <el-radio-button value="ios">🍎 iOS</el-radio-button>
                  <el-radio-button value="harmony">🔷 鸿蒙</el-radio-button>
                </el-radio-group>
              </div>
              <!-- 商店列表 -->
              <div class="guide-store-list">
                <el-collapse v-model="guideExpandedStores">
                  <el-collapse-item
                    v-for="store in guideFilteredStores"
                    :key="store.type"
                    :name="store.type"
                    class="guide-store-item"
                  >
                    <template #title>
                      <div class="guide-store-header">
                        <span class="guide-store-name">{{ store.label }}</span>
                        <el-tag size="small" :type="isStoreEnabled(store.type) ? 'success' : 'info'">
                          {{ isStoreEnabled(store.type) ? '已配置' : '未配置' }}
                        </el-tag>
                      </div>
                    </template>
                    <div class="guide-store-body">
                      <div class="guide-store-steps">
                        <div v-for="(step, i) in store.guideSteps" :key="i" class="guide-step-row">
                          <div class="guide-step-badge">{{ i + 1 }}</div>
                          <div>
                            <div class="guide-step-title">{{ step.title }}</div>
                            <div class="guide-step-desc">{{ step.description }}</div>
                          </div>
                        </div>
                      </div>
                      <el-image
                        v-if="store.guideImage"
                        :src="store.guideImage"
                        fit="contain"
                        class="guide-store-image"
                        preview-teleported
                      />
                      <div class="guide-store-hint">{{ store.guideHint }}</div>
                      <div class="guide-store-link">
                        <div class="guide-store-link-label">应用跳转链接</div>
                        <div class="guide-store-link-hint">{{ store.jumpLinkHint }}</div>
                        <el-link :href="store.guideUrl" target="_blank" type="primary" size="small">查看官方文档 →</el-link>
                      </div>
                    </div>
                  </el-collapse-item>
                </el-collapse>
              </div>
            </el-tab-pane>
          </el-tabs>
        </el-tab-pane>

        <!-- Publish Config -->
        <el-tab-pane label="发布配置" name="publish-config">
          <el-alert
            title="这里配置灰度默认模式以及成员同步 / 选择回调。发布时仍然会让你最后确认。"
            type="info"
            show-icon
            :closable="false"
            style="margin-bottom:16px"
          />
          <div class="toolbar responsive-toolbar">
            <el-button @click="loadPublishConfig" :loading="loadingPublishConfig">刷新</el-button>
            <el-button type="primary" @click="savePublishConfig" :loading="savingPublishConfig">保存配置</el-button>
          </div>

          <el-form :model="publishConfigForm" label-width="170px" class="release-config-form">
            <el-form-item label="更新免登录">
              <el-switch v-model="publishConfigForm.allowAnonymousUpdateCheck" />
            </el-form-item>
            <el-form-item label="实时更新通知">
              <el-switch v-model="publishConfigForm.enableRealtimeNotification" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
              <span class="form-tip" style="margin-left:8px">开启后发布新版本时，已打开的 App 会立即收到通知</span>
            </el-form-item>
            <el-alert
              v-if="publishConfigForm.allowAnonymousUpdateCheck"
              type="warning"
              :closable="false"
              show-icon
              title="开启后，未登录设备也可以检查更新，但灰度发布相关功能将被禁用。"
              style="margin-bottom:16px"
            />
            <el-form-item label="默认灰度模式">
              <el-radio-group v-model="publishConfigForm.grayMode" :disabled="publishConfigForm.allowAnonymousUpdateCheck">
                <el-radio-button value="PERCENT">比例</el-radio-button>
                <el-radio-button value="MEMBERS">成员</el-radio-button>
              </el-radio-group>
            </el-form-item>
            <el-form-item v-if="publishConfigForm.grayMode === 'PERCENT'" label="默认灰度比例">
              <el-slider v-model="publishConfigForm.defaultGrayPercent" :min="1" :max="100" show-input :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
            </el-form-item>
            <template v-if="publishConfigForm.grayMode === 'MEMBERS'">
              <el-form-item label="成员同步地址">
                <el-input v-model="publishConfigForm.graySyncUrl" placeholder="集成方成员列表接口地址（留空则使用 AppSecret 签名）" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
              </el-form-item>
              <el-form-item label="发布时回调地址">
                <el-input v-model="publishConfigForm.publishCallbackUrl" placeholder="发版灰度时回调集成方获取成员列表（可选）" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
              </el-form-item>
            </template>
          </el-form>
        </el-tab-pane>

        <!-- Operation Logs -->
        <el-tab-pane label="操作记录" name="logs">
          <div class="toolbar responsive-toolbar">
            <el-button @click="loadOperationLogs" :loading="loadingOperationLogs">刷新</el-button>
          </div>
          <div class="table-wrap">
          <el-table :data="operationLogs" v-loading="loadingOperationLogs" border stripe>
            <el-table-column prop="createdAt" label="时间" width="170">
              <template #default="{row}">{{ formatTime(row.createdAt) }}</template>
            </el-table-column>
            <el-table-column prop="resourceType" label="对象" width="120">
              <template #default="{row}">{{ operationResourceLabel(row.resourceType) }}</template>
            </el-table-column>
            <el-table-column prop="action" label="操作" width="140">
              <template #default="{row}">{{ operationActionLabel(row.action) }}</template>
            </el-table-column>
            <el-table-column prop="operator" label="操作人" width="140" />
            <el-table-column prop="reason" label="原因" width="220" show-overflow-tooltip />
            <el-table-column prop="detailJson" label="详情" show-overflow-tooltip>
              <template #default="{row}">{{ formatDetail(row.detailJson) }}</template>
            </el-table-column>
          </el-table>
          </div>
        </el-tab-pane>

        <!-- Statistics -->
        <el-tab-pane label="统计" name="stats">
          <div class="toolbar responsive-toolbar">
            <el-radio-group v-model="statsPlatform" @change="loadStats" style="margin-right:12px">
              <el-radio-button value="ANDROID">Android</el-radio-button>
              <el-radio-button value="IOS">iOS</el-radio-button>
              <el-radio-button value="HARMONY">Harmony</el-radio-button>
            </el-radio-group>
            <el-select v-model="statsDays" style="width:120px;margin-right:12px" @change="loadStats">
              <el-option :value="7" label="近 7 天" />
              <el-option :value="14" label="近 14 天" />
              <el-option :value="30" label="近 30 天" />
            </el-select>
            <el-button @click="loadStats" :loading="loadingStats">刷新</el-button>
          </div>

          <div v-loading="loadingStats" class="stats-grid">
            <el-card shadow="never" class="stats-card">
              <template #header><span>版本分布</span></template>
              <div ref="versionChartRef" class="chart-container"></div>
              <el-empty v-if="!loadingStats && statsVersionData.length === 0" description="暂无数据" :image-size="60" />
            </el-card>

            <el-card shadow="never" class="stats-card">
              <template #header><span>近 {{ statsDays }} 天检测趋势</span></template>
              <div ref="trendChartRef" class="chart-container"></div>
              <el-empty v-if="!loadingStats && statsTrendData.length === 0" description="暂无数据" :image-size="60" />
            </el-card>

            <el-card shadow="never" class="stats-card">
              <template #header><span>设备型号 Top 10</span></template>
              <div ref="modelChartRef" class="chart-container"></div>
              <el-empty v-if="!loadingStats && statsModelData.length === 0" description="暂无数据" :image-size="60" />
            </el-card>

            <el-card shadow="never" class="stats-card">
              <template #header><span>系统版本分布</span></template>
              <div ref="osChartRef" class="chart-container"></div>
              <el-empty v-if="!loadingStats && statsOsData.length === 0" description="暂无数据" :image-size="60" />
            </el-card>

            <el-card shadow="never" class="stats-card">
              <template #header><span>厂商分布</span></template>
              <div ref="vendorChartRef" class="chart-container"></div>
              <el-empty v-if="!loadingStats && statsVendorData.length === 0" description="暂无数据" :image-size="60" />
            </el-card>
          </div>
        </el-tab-pane>
      </el-tabs>
    </el-card>

    <!-- Publish Dialog -->
    <el-dialog v-model="showPublish" title="发布 / 重新上架" :width="dialogWidth">
      <el-form label-width="120px">
        <el-form-item label="发布方式">
          <el-radio-group v-model="publishForm.publishImmediately">
            <el-radio-button :value="true">立即发布</el-radio-button>
            <el-radio-button :value="false">定时发布</el-radio-button>
          </el-radio-group>
        </el-form-item>
        <el-form-item v-if="!publishForm.publishImmediately" label="计划发布时间">
          <el-date-picker
            v-model="publishForm.scheduledPublishAt"
            type="datetime"
            placeholder="选择发布时间"
            format="YYYY-MM-DD HH:mm:ss"
            value-format="YYYY-MM-DDTHH:mm:ss"
            clearable
          />
        </el-form-item>
        <el-form-item v-if="publishTarget?.type === 'app'" label="强制更新">
          <el-switch v-model="publishForm.forceUpdate" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showPublish = false">取消</el-button>
        <el-button type="primary" @click="submitPublish" :loading="submittingPublish">确认</el-button>
      </template>
    </el-dialog>

    <!-- Gray Release Dialog -->
    <el-dialog v-model="showGray" title="灰度发布配置" width="600px">
      <el-form label-width="110px">
        <el-form-item label="开启灰度"><el-switch v-model="grayForm.enabled" /></el-form-item>
        <el-form-item label="灰度方式" v-if="grayForm.enabled">
          <el-radio-group v-model="grayForm.grayMode">
            <el-radio-button value="PERCENT">比例灰度</el-radio-button>
            <el-radio-button value="MEMBERS">指定成员</el-radio-button>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="灰度比例" v-if="grayForm.enabled && grayForm.grayMode === 'PERCENT'">
          <el-slider v-model="grayForm.percent" :min="1" :max="100" show-input />
        </el-form-item>
        <template v-if="grayForm.enabled && grayForm.grayMode === 'MEMBERS'">
          <!-- 按标签选择 -->
          <el-form-item label="按标签选择">
            <div style="width:100%">
              <el-checkbox-group v-model="grayForm.groupNames">
                <div v-for="tag in grayTags" :key="tag.tagName" class="gray-tag-item">
                  <el-checkbox :value="tag.tagName">
                    {{ tag.tagName }} <el-tag size="small" type="info">{{ tag.memberCount }}人</el-tag>
                  </el-checkbox>
                </div>
              </el-checkbox-group>
              <el-empty v-if="!grayTags.length" description="暂无标签，请先在成员管理中创建" :image-size="40" />
              <div style="margin-top:8px;display:flex;gap:8px">
                <el-button size="small" @click="importGrayFromIm" :loading="importingIm">从 IM 导入成员</el-button>
              </div>
            </div>
          </el-form-item>
          <!-- 额外指定成员 -->
          <el-form-item label="追加指定成员">
            <el-input
              v-model="grayExtraInput"
              placeholder="输入 userId，多个用逗号分隔"
              clearable
              @blur="addExtraMembers"
              style="width: 100%"
            />
            <div v-if="grayForm.extraMemberIds.length" class="gray-extra-tags">
              <el-tag
                v-for="uid in grayForm.extraMemberIds"
                :key="uid"
                closable
                @close="removeExtraMember(uid)"
                size="small"
              >{{ uid }}</el-tag>
            </div>
          </el-form-item>
          <!-- 汇总 -->
          <el-form-item label="灰度人数">
            <span class="gray-summary">
              标签 {{ grayTagMemberCount }} 人 + 指定 {{ grayForm.extraMemberIds.length }} 人
              <template v-if="grayOverlapCount > 0">（去重 {{ grayOverlapCount }} 人）</template>
              = <strong>{{ grayTotalCount }}</strong> 人
            </span>
          </el-form-item>
        </template>
      </el-form>
      <template #footer>
        <el-button @click="showGray = false">取消</el-button>
        <el-button type="primary" @click="submitGray" :loading="submittingGray">保存</el-button>
      </template>
    </el-dialog>

    <!-- Submit to Store Dialog -->
    <el-dialog v-model="showSubmitStore" title="提交应用市场" :width="dialogWidth">
      <div v-if="submitStoreVersion">
        <p style="margin-bottom:12px">
          版本 <strong>{{ submitStoreVersion.versionName }}</strong>
          将提交至以下已配置且启用的市场：
        </p>
        <el-form label-width="120px" style="margin-bottom:12px">
          <el-form-item label="上架方式">
            <el-radio-group v-model="submitStoreMode">
              <el-radio-button value="AUTO_REVIEW">立即上架</el-radio-button>
              <el-radio-button value="SCHEDULED">计划上架</el-radio-button>
            </el-radio-group>
          </el-form-item>
          <el-form-item v-if="submitStoreMode === 'SCHEDULED'" label="计划时间">
            <el-date-picker
              v-model="submitStoreScheduledAt"
              type="datetime"
              placeholder="选择上架时间"
              format="YYYY-MM-DD HH:mm:ss"
              value-format="YYYY-MM-DDTHH:mm:ss"
              clearable
            />
          </el-form-item>
      </el-form>
        <el-checkbox-group v-model="selectedStores">
          <div v-for="store in submissionStoreDefs" :key="store.type" class="store-checkbox-row">
            <el-checkbox
              :value="store.type"
              :disabled="isStoreActiveReview(submitStoreVersion, store.type)"
            >{{ store.label }}</el-checkbox>
            <el-tag size="small" type="success" style="margin-left:8px">已配置</el-tag>
            <el-tag
              v-if="getSubmitDialogStoreState(submitStoreVersion, store.type)"
              :type="reviewTagType(getSubmitDialogStoreState(submitStoreVersion, store.type))"
              size="small"
              style="margin-left:4px"
            >{{ reviewLabel(getSubmitDialogStoreState(submitStoreVersion, store.type)) }}</el-tag>
            <span
              v-if="isStoreActiveReview(submitStoreVersion, store.type)"
              style="font-size:12px;color:var(--el-text-color-secondary);margin-left:6px"
            >{{ getSubmitDialogStoreState(submitStoreVersion, store.type) === 'SUBMITTING' ? '上传中，将跳过' : '审核进行中，将跳过' }}</span>
          </div>
        </el-checkbox-group>
        <el-alert
          v-if="!submissionStoreDefs.length"
          type="warning"
          show-icon
          :closable="false"
          title="当前应用暂未配置可用于上架的市场，请先在「应用商店配置」标签页中配置。"
          style="margin-top:12px"
        />
      </div>
      <template #footer>
        <el-button @click="showSubmitStore = false">取消</el-button>
        <el-button
          type="primary"
          :disabled="!selectedStores.length"
          @click="confirmSubmitToStores"
          :loading="submittingToStores"
        >提交审核</el-button>
      </template>
    </el-dialog>

    <!-- Store Review Detail Dialog -->
    <el-dialog v-model="showStoreReviewDetail" title="应用商店状态详情" :width="isMobile ? '100%' : '760px'" @close="stopDialogPoll">
      <div v-if="storeReviewDetailVersion">
        <!-- Version summary bar -->
        <div class="review-detail-header">
          <span class="review-detail-version">
            {{ storeReviewDetailVersion.versionName }}
            <span class="review-detail-code">· {{ storeReviewDetailVersion.versionCode }}</span>
          </span>
          <el-tag :type="statusTagType(storeReviewDetailVersion)" size="small">
            {{ statusLabel(storeReviewDetailVersion) }}
          </el-tag>
          <span v-if="storeReviewDetailLive" class="review-live-dot" title="实时更新中">
            <span class="live-pulse" />实时
          </span>
        </div>

        <!-- Per-store cards -->
        <div class="store-review-cards">
          <div
            v-for="item in storeReviewDetailItems"
            :key="item.store"
            class="review-store-card"
            :class="[`review-card--${item.state.toLowerCase()}`, { 'review-card--active': isActiveState(item.state) }]"
          >
            <!-- Card header: store name + status -->
            <div class="review-card-head">
              <span class="review-card-name">{{ storeLabel(item.store) }}</span>
              <el-tag :type="reviewTagType(item.state)" size="small" class="review-card-tag">
                {{ reviewDisplayLabel(item) }}
              </el-tag>
            </div>

            <!-- Stage progress -->
            <div class="review-card-progress">
              <div
                v-for="step in reviewSteps"
                :key="step.stage"
                class="review-step"
                :class="reviewStepClass(item.state, item.stage, step)"
              >
                <div class="review-step-dot">
                  <el-icon v-if="reviewStepDone(item.state, step)"><CircleCheckFilled /></el-icon>
                  <el-icon v-else-if="reviewStepActive(item.state, item.stage, step)" class="is-loading"><Loading /></el-icon>
                  <span v-else class="step-dot-empty" />
                </div>
                <span class="review-step-label">{{ step.label }}</span>
              </div>
            </div>

            <!-- Meta info -->
            <div class="review-card-meta">
              <div v-if="item.submittedAt" class="review-card-meta-item">
                <span class="meta-label">提交时间</span>
                <span class="meta-value">{{ formatTime(item.submittedAt) }}</span>
              </div>
              <div v-if="item.updatedAt" class="review-card-meta-item">
                <span class="meta-label">更新时间</span>
                <span class="meta-value">{{ formatTime(item.updatedAt) }}</span>
              </div>
              <div v-if="item.batchId" class="review-card-meta-item">
                <span class="meta-label">批次</span>
                <span class="meta-value meta-mono">{{ item.batchId.slice(0, 8) }}…</span>
              </div>
              <div v-if="item.nonCurrentRelease || item.currentSubmissionLive === false" class="review-card-meta-item">
                <span class="meta-label">发布归属</span>
                <span class="meta-value">非本次发布</span>
              </div>
              <div v-if="item.liveVersionName || item.liveVersionCode" class="review-card-meta-item">
                <span class="meta-label">线上版本</span>
                <span class="meta-value">{{ [item.liveVersionName, item.liveVersionCode].filter(Boolean).join(' · ') }}</span>
              </div>
              <div v-if="item.reviewVersionName || item.reviewVersionCode" class="review-card-meta-item">
                <span class="meta-label">审核版本</span>
                <span class="meta-value">{{ [item.reviewVersionName, item.reviewVersionCode].filter(Boolean).join(' · ') }}</span>
              </div>
            </div>

            <!-- Rejection / withdrawal reason -->
            <div v-if="item.reason && (item.state === 'REJECTED' || item.state === 'WITHDRAWN' || item.state === 'FAILED' || item.nonCurrentRelease || item.currentSubmissionLive === false)" class="review-card-reason">
              <el-icon><WarningFilled /></el-icon>
              <span>{{ item.reason }}</span>
            </div>

            <!-- Retry button for failed or rejected submissions -->
            <div v-if="item.state === 'FAILED' || item.state === 'REJECTED'" class="review-card-retry">
              <el-button
                type="warning"
                size="small"
                :loading="retryingStores.has(item.store)"
                @click="handleRetryStore(item.store)"
              >重新提交</el-button>
            </div>

          </div>
        </div>

        <el-empty v-if="!storeReviewDetailItems.length" description="暂无商店状态数据" />
      </div>
      <el-empty v-else description="暂无商店状态数据" />
      <template #footer>
        <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
          <el-button
            type="primary"
            plain
            :loading="refreshingReviewStatus"
            @click="handleRefreshReviewStatus()"
          >刷新厂商状态</el-button>
          <el-button
            v-if="storeReviewDetailItems.some(i => isActiveState(i.state))"
            type="danger"
            plain
            :loading="cancellingReview"
            @click="handleCancelReview()"
          >撤回审核</el-button>
          <el-button
            v-if="storeReviewDetailItems.some(i => i.state === 'APPROVED')"
            type="warning"
            plain
            @click="showPublishSchedule = true"
          >修改发布计划</el-button>
          <el-button style="margin-left:auto" type="primary" @click="showStoreReviewDetail = false; stopDialogPoll()">关闭</el-button>
        </div>
      </template>
    </el-dialog>

    <!-- Publish Schedule Dialog -->
    <el-dialog v-model="showPublishSchedule" title="修改发布计划" width="420px">
      <el-form label-width="120px" style="margin-top:8px">
        <el-form-item label="发布方式">
          <el-radio-group v-model="publishScheduleType">
            <el-radio value="IMMEDIATE">立即发布</el-radio>
            <el-radio value="SCHEDULED">定时发布</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item v-if="publishScheduleType === 'SCHEDULED'" label="发布时间">
          <el-date-picker
            v-model="publishScheduleAt"
            type="datetime"
            placeholder="选择发布时间"
            format="YYYY-MM-DD HH:mm"
            value-format="YYYY-MM-DDTHH:mm:ss"
            style="width:100%"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showPublishSchedule = false">取消</el-button>
        <el-button type="primary" :loading="updatingPublishSchedule" @click="handleUpdatePublishSchedule">确定</el-button>
      </template>
    </el-dialog>

    <!-- Changelog Edit Dialog -->
    <el-dialog v-model="showChangelogEdit" title="修改更新说明" width="480px">
      <el-input
        v-model="changelogEditValue"
        type="textarea"
        :rows="5"
        placeholder="请输入更新说明（可留空）"
        maxlength="2000"
        show-word-limit
      />
      <template #footer>
        <el-button @click="showChangelogEdit = false">取消</el-button>
        <el-button type="primary" :loading="savingChangelog" @click="submitChangelogEdit">保存</el-button>
      </template>
    </el-dialog>

    <!-- Store Credential Config Dialog -->
    <el-dialog
      v-model="showStoreConfig"
      :title="`配置 ${currentStoreDef?.label} 凭据`"
      :width="dialogWidth"
    >
      <el-form v-if="currentStoreDef" :model="storeConfigForm" label-width="150px" class="store-config-form">
        <el-form-item label="启用">
          <el-switch v-model="storeConfigForm.enabled" />
        </el-form-item>
        <template v-for="field in currentStoreDef.fields" :key="field.key">
          <el-form-item
            v-if="!field.visibleWhen || field.visibleWhen(storeConfigForm.values)"
            :label="field.label"
          >
            <el-select
              v-if="field.type === 'select'"
              v-model="storeConfigForm.values[field.key]"
              style="width:100%"
              clearable
              placeholder="请选择"
            >
              <el-option
                v-for="opt in field.options"
                :key="opt.value"
                :label="opt.label"
                :value="opt.value"
              />
            </el-select>
            <el-input
              v-else-if="field.type === 'password'"
              v-model="storeConfigForm.values[field.key]"
              type="password"
              show-password
              :placeholder="field.placeholder"
            />
            <el-input
              v-else-if="field.type === 'textarea'"
              v-model="storeConfigForm.values[field.key]"
              type="textarea"
              :rows="4"
              :placeholder="field.placeholder"
            />
            <el-input
              v-else
              v-model="storeConfigForm.values[field.key]"
              :placeholder="field.placeholder"
            />
          </el-form-item>
        </template>
      </el-form>
      <template #footer>
        <el-button @click="showStoreConfig = false">取消</el-button>
        <el-button type="primary" @click="saveStoreConfig" :loading="savingStoreConfig">保存</el-button>
      </template>
    </el-dialog>

    <!-- Upload App Version Dialog -->
    <el-dialog v-model="showUploadApp" title="上传 App 版本" :width="dialogWidth">
      <el-form :model="appUploadForm" label-width="120px">
        <el-form-item label="平台">
          <el-select v-model="appUploadForm.platform" @change="handleAppPlatformChange">
            <el-option value="ANDROID" label="Android" />
            <el-option value="IOS" label="iOS" />
            <el-option value="HARMONY" label="Harmony" />
          </el-select>
        </el-form-item>
        <el-form-item label="包名 / Bundle ID">
          <el-input v-model="appUploadForm.packageName" readonly />
        </el-form-item>
        <el-form-item label="版本名称"><el-input v-model="appUploadForm.versionName" placeholder="选择文件后可自动填充" /></el-form-item>
        <el-form-item label="版本码"><el-input-number v-model="appUploadForm.versionCode" :min="1" /></el-form-item>
        <el-form-item label="更新说明"><el-input v-model="appUploadForm.changeLog" type="textarea" :rows="3" /></el-form-item>
        <el-form-item v-if="appUploadForm.platform === 'IOS'" label="App Store 链接（可选）">
          <el-input v-model="appUploadForm.appStoreUrl" placeholder="可自动从应用商店配置回显" />
        </el-form-item>
        <el-form-item v-if="appUploadForm.platform === 'HARMONY'" label="应用市场链接（可选）">
          <el-input v-model="appUploadForm.marketUrl" placeholder="鸿蒙应用市场详情页链接" />
        </el-form-item>
        <el-form-item v-if="appUploadForm.platform === 'ANDROID'" label="包文件">
          <el-upload
            v-if="!appUploadForm.fileUrl"
            ref="appUploadRef"
            class="apk-dropzone"
            drag
            :auto-upload="false"
            :limit="1"
            :on-change="onAppPackageChange"
            accept=".apk"
          >
            <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
            <div class="el-upload__text">
              将 APK 拖到这里，或 <em>点击选择文件</em>
            </div>
            <template #tip>
              <div class="el-upload__tip">
                选择或拖入文件后会自动上传并识别版本信息。
              </div>
            </template>
          </el-upload>
          <div v-else class="uploaded-file-info">
            <el-icon class="file-icon"><Document /></el-icon>
            <span class="file-name">{{ appUploadForm.file?.name || '已上传文件' }}</span>
            <el-button type="danger" link size="small" @click="removeUploadedFile">
              <el-icon><Delete /></el-icon> 删除
            </el-button>
          </div>
        </el-form-item>
        <el-form-item v-if="appUploadForm.platform === 'ANDROID' && appPackageUploadProgress > 0" label="文件进度">
          <div class="upload-progress-block">
            <el-progress :percentage="appPackageUploadProgress" :status="appPackageUploadProgress === 100 ? 'success' : undefined" />
            <span class="upload-progress-text">{{ appPackageUploadProgress }}%</span>
          </div>
        </el-form-item>
        <el-alert
          v-if="appUploadForm.platform !== 'ANDROID'"
          type="info"
          :closable="false"
          show-icon
          title="iOS 和鸿蒙只记录版本信息，不需要上传安装包。商店跳转链接可选填写，方便后续跳转与回显。"
        />
        <el-alert
          v-if="appUploadForm.platform === 'ANDROID'"
          type="info"
          :closable="false"
          show-icon
          title="选中 APK 后会先上传到文件服务，再读取包名、版本名和版本码；若识别到的包名与当前应用不一致，可选择强制继续使用。"
        />
        <el-form-item v-if="appVersionUploadProgress > 0" label="提交进度">
          <div class="upload-progress-block">
            <el-progress :percentage="appVersionUploadProgress" :status="appVersionUploadProgress === 100 ? 'success' : undefined" />
            <span class="upload-progress-text">{{ appVersionUploadProgress }}%</span>
          </div>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showUploadApp = false">取消</el-button>
        <el-button type="primary" @click="submitAppUpload" :loading="uploadingApp">上传</el-button>
      </template>
    </el-dialog>

    <!-- Upload RN Bundle Dialog -->
    <el-dialog v-model="showUploadRn" title="上传 RN Bundle" :width="dialogWidth">
      <el-form :model="rnUploadForm" label-width="120px">
        <el-form-item label="Bundle 文件">
          <el-upload
            class="bundle-dropzone"
            drag
            :auto-upload="false"
            :limit="1"
            :on-change="onRnBundleChange"
            accept=".xuqm.zip"
          >
            <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
            <div class="el-upload__text">
              将 CLI 生成的 .xuqm.zip 发布包拖到这里，或 <em>点击选择文件</em>
            </div>
            <template #tip>
              <div class="el-upload__tip">
                发布身份只读取压缩包根目录的 rn-manifest.json，页面不能修改。
              </div>
            </template>
          </el-upload>
        </el-form-item>
        <el-alert
          type="info"
          :closable="false"
          show-icon
          title="只接受由发布 CLI 生成的 .xuqm.zip。服务端会校验清单、Bundle SHA-256、归档 SHA-256、应用归属并签署不可变发布身份。"
        />
        <el-form-item label="说明"><el-input v-model="rnUploadForm.note" type="textarea" :rows="2" /></el-form-item>
        <el-form-item v-if="rnBundleUploadProgress > 0" label="提交进度">
          <div class="upload-progress-block">
            <el-progress :percentage="rnBundleUploadProgress" :status="rnBundleUploadProgress === 100 ? 'success' : undefined" />
            <span class="upload-progress-text">{{ rnBundleUploadProgress }}%</span>
          </div>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showUploadRn = false">取消</el-button>
        <el-button type="primary" @click="submitRnUpload" :loading="uploadingRn">上传</el-button>
      </template>
    </el-dialog>

    <!-- Global Drag Drop Overlay -->
    <div v-if="isDraggingOver" class="drag-overlay">
      <div class="drag-overlay-content">
        <el-icon size="64"><UploadFilled /></el-icon>
        <p class="drag-overlay-title">释放文件以上传</p>
        <p class="drag-overlay-hint">支持 .apk、.xuqm.zip</p>
      </div>
    </div>
  </template>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { CircleCheckFilled, Delete, Document, Edit, Loading, UploadFilled, WarningFilled } from '@element-plus/icons-vue'
import { appApi, type App } from '@/api/app'
import { fileApi } from '@/api/file'
import { formatTime } from '@/utils/date'
import { getLastServiceApp, saveLastServiceApp } from '@/utils/serviceApp'
import {
  updateAdminApi,
  type AppPackageInspectResult,
  type AppVersion,
  type GrayMode,
  type GrayTag,
  type PublishMode,
  type RnBundle,
  type StoreConfig,
  type StoreType,
} from '@/api/update'
import {
  connectStoreReviewRealtime,
  disconnectStoreReviewRealtime,
  notifyStoreReviewRefresh,
  isStoreReviewRealtimeConnected,
  type StoreReviewRefreshEvent,
} from '@/services/storeReviewRealtime'
import huaweiGuideImage from '@/assets/update-store/huawei/01.png'
import miGuideImage from '@/assets/update-store/mi/01.png'
import oppoGuideImage from '@/assets/update-store/oppo/01.png'
import vivoGuideImage from '@/assets/update-store/vivo/01.png'
import honorGuideImage from '@/assets/update-store/honor/01.png'

const route = useRoute()
const router = useRouter()
const appKey = computed(() => (route.params.appKey as string) ?? '')
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
const portalApps = ref<App[]>([])

const serviceEnabled = ref<boolean | null>(null)
const checkingService = ref(false)
const showActivationDialog = ref(false)
const activationReason = ref('')
const submittingActivation = ref(false)

async function copyDownloadPageLink() {
  if (!appKey.value) {
    ElMessage.warning('缺少应用标识')
    return
  }
  const url = `${window.location.origin}/download/${appKey.value}`
  try {
    await navigator.clipboard.writeText(url)
    ElMessage.success(`已复制对外下载页链接：${url}`)
  } catch {
    ElMessage.info(url)
  }
}

async function checkServiceEnabled() {
  checkingService.value = true
  serviceEnabled.value = null
  try {
    const res = await appApi.getServices(appKey.value)
    serviceEnabled.value = res.data.data.some(s => s.serviceType === 'UPDATE' && s.enabled)
  } catch {
    serviceEnabled.value = false
  } finally {
    checkingService.value = false
  }
}

async function submitActivation() {
  if (!activationReason.value.trim()) {
    ElMessage.warning('请填写申请理由')
    return
  }
  submittingActivation.value = true
  try {
    await appApi.requestActivation(appKey.value, 'UPDATE', activationReason.value.trim())
    ElMessage.success('申请已提交，等待运营审核')
    showActivationDialog.value = false
    activationReason.value = ''
  } catch {
    ElMessage.error('提交失败')
  } finally {
    submittingActivation.value = false
  }
}
const app = ref<App | null>(null)
const pageTitle = computed(() => app.value?.name ?? appKey.value)

const platformPackageName = computed(() => {
  if (!app.value) return ''
  const map: Record<string, string | undefined> = {
    ANDROID: app.value.packageName,
    IOS: app.value.iosBundleId,
    HARMONY: app.value.harmonyBundleName,
  }
  return map[appPlatform.value] ?? ''
})
const isMobile = ref(false)
const dialogWidth = computed(() => (isMobile.value ? 'calc(100vw - 24px)' : '920px'))

const isDraggingOver = ref(false)
let dragCounter = 0

function handleDragEnter(e: DragEvent) {
  e.preventDefault()
  dragCounter++
  isDraggingOver.value = true
}

function handleDragOver(e: DragEvent) {
  e.preventDefault()
  if (e.dataTransfer) {
    e.dataTransfer.dropEffect = 'copy'
  }
}

function handleDragLeave(e: DragEvent) {
  dragCounter--
  if (dragCounter <= 0) {
    isDraggingOver.value = false
    dragCounter = 0
  }
}

async function handleDrop(e: DragEvent) {
  e.preventDefault()
  isDraggingOver.value = false
  dragCounter = 0

  const files = Array.from(e.dataTransfer?.files || [])
  if (!files.length) return

  const file = files[0]
  const ext = file.name.slice(file.name.lastIndexOf('.')).toLowerCase()

  if (ext === '.apk') {
    openUploadAppDialog()
    await nextTick()
    await onAppPackageChange({ raw: file })
  } else if (file.name.toLowerCase().endsWith('.xuqm.zip')) {
    showUploadRn.value = true
    await nextTick()
    await onRnBundleChange({ raw: file })
  } else {
    ElMessage.warning(`不支持的文件类型：${file.name}，请拖入 .apk 或 .xuqm.zip 文件`)
  }
}

const activeTab = ref('app')
const storeTab = ref<'configs' | 'guide'>('configs')
const appPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY'>('ANDROID')
const rnPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY' | ''>('')
const rnModuleFilter = ref('')

const appVersions = ref<AppVersion[]>([])
const loadingApp = ref(false)
const rnBundles = ref<RnBundle[]>([])
const loadingRn = ref(false)
const storeConfigs = ref<StoreConfig[]>([])
const loadingPublishConfig = ref(false)
const savingPublishConfig = ref(false)
const publishConfigForm = ref({
  allowAnonymousUpdateCheck: false,
  defaultGrayPercent: 0,
  grayMode: 'PERCENT' as GrayMode,
  graySyncUrl: '',
  publishCallbackUrl: '',
  enableRealtimeNotification: false,
  parallelStoreUpload: true,
})
const appPackageInspecting = ref(false)
const appPackageUploadProgress = ref(0)
const appVersionUploadProgress = ref(0)
const appUploadRef = ref<any>(null)
const rnBundleUploadProgress = ref(0)
const operationLogs = ref<{
  id: string
  appKey: string
  resourceType: string
  resourceId: string
  action: string
  operator?: string
  reason?: string
  detailJson?: string
  createdAt: string
}[]>([])
const loadingOperationLogs = ref(false)

// ── Statistics ──────────────────────────────────────────────────────────
import * as echarts from 'echarts'

const statsPlatform = ref<'ANDROID' | 'IOS' | 'HARMONY'>('ANDROID')
const statsDays = ref(7)
const loadingStats = ref(false)
const statsVersionData = ref<{ versionCode: number; versionName: string; deviceCount: number }[]>([])
const statsTrendData = ref<{ day: string; deviceCount: number }[]>([])
const statsModelData = ref<{ model: string; deviceCount: number }[]>([])
const statsOsData = ref<{ osVersion: string; deviceCount: number }[]>([])
const statsVendorData = ref<{ vendor: string; deviceCount: number }[]>([])

const versionChartRef = ref<HTMLElement | null>(null)
const trendChartRef = ref<HTMLElement | null>(null)
const modelChartRef = ref<HTMLElement | null>(null)
const osChartRef = ref<HTMLElement | null>(null)
const vendorChartRef = ref<HTMLElement | null>(null)

let versionChart: echarts.ECharts | null = null
let trendChart: echarts.ECharts | null = null
let modelChart: echarts.ECharts | null = null
let osChart: echarts.ECharts | null = null
let vendorChart: echarts.ECharts | null = null

let storeReviewReloadTimer: ReturnType<typeof setTimeout> | null = null
let storeReviewAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
let storeReviewDialogPollTimer: ReturnType<typeof setInterval> | null = null

const allowAnonymousUpdateCheck = computed(() => Boolean(publishConfigForm.value.allowAnonymousUpdateCheck))

type FieldDef = {
  key: string
  label: string
  type?: 'password' | 'textarea' | 'select'
  placeholder?: string
  options?: { label: string; value: string }[]
  visibleWhen?: (values: Record<string, string>) => boolean
}
type GuideStep = { title: string; description: string }
function marketUrlField(placeholder: string): FieldDef {
  return { key: 'marketUrl', label: '应用市场跳转页面（可选）', placeholder }
}
type StoreDef = {
  type: StoreType
  label: string
  shortLabel: string
  fields: FieldDef[]
  guideSubtitle: string
  guideUrl: string
  guideSteps: GuideStep[]
  jumpLinkHint: string
  guideHint: string
  guideImage?: string
}

const STORE_DEFS: StoreDef[] = [
  {
    type: 'HUAWEI',
    label: '华为应用市场',
    shortLabel: '华为',
    fields: [
      { key: 'clientId', label: 'Client ID', placeholder: 'AppGallery Connect Client ID' },
      { key: 'clientSecret', label: 'Client Secret', type: 'password', placeholder: 'AppGallery Connect Client Secret' },
      marketUrlField('华为应用市场详情页链接'),
    ],
    guideSubtitle: '在 AppGallery Connect 创建 Connect API 凭据',
    guideUrl: 'https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-Guides/agcapi-getstarted-0000001111845114',
    guideSteps: [
      { title: '创建应用或进入应用管理', description: '在 AppGallery Connect 中找到目标应用。' },
      { title: '进入开发工具 / Connect API', description: '创建服务端凭据并选择 APP 管理员角色。' },
      { title: '复制客户端 ID 和密钥', description: '把 Client ID / Client Secret 填到这里。' },
    ],
    jumpLinkHint: '华为应用的详情页链接一般形如 appgallery.huawei.com/app/detail?id=包名，可直接在应用详情页复制。',
    guideHint: '与后端 submitToHuawei 读取逻辑一致：clientId / clientSecret + marketUrl。',
    guideImage: huaweiGuideImage,
  },
  {
    type: 'MI',
    label: '小米应用商店',
    shortLabel: '小米',
    fields: [
      { key: 'username', label: '用户名' },
      { key: 'publicKey', label: '公钥证书', type: 'textarea', placeholder: '-----BEGIN CERTIFICATE-----\n...' },
      { key: 'privateKey', label: 'RSA 私钥', type: 'textarea', placeholder: '-----BEGIN PRIVATE KEY-----\n...' },
      marketUrlField('小米应用商店详情页链接'),
    ],
    guideSubtitle: '在自动发布接口页面获取上传所需密钥',
    guideUrl: 'https://dev.mi.com/distribute/doc/details?pId=1134',
    guideSteps: [
      { title: '进入应用游戏管理', description: '在控制台选择目标应用。' },
      { title: '打开自动发布接口', description: '下载公钥文件并准备私钥。' },
      { title: '录入用户名和私钥', description: '这里保存的是服务端上传所需凭据。' },
    ],
    jumpLinkHint: '小米应用商店详情页可从应用的商店公开链接或发布页面复制，通常包含 appKey 或 package 信息。',
    guideHint: '当前字段为 username / publicKey / privateKey，与后端服务一致。',
    guideImage: miGuideImage,
  },
  {
    type: 'OPPO',
    label: 'OPPO 软件商店',
    shortLabel: 'OPPO',
    fields: [
      { key: 'clientId', label: 'Client ID' },
      { key: 'clientSecret', label: 'Client Secret', type: 'password' },
      marketUrlField('OPPO 软件商店详情页链接'),
    ],
    guideSubtitle: '在我的 API 里创建服务端应用',
    guideUrl: 'https://open.oppomobile.com/new/developmentDoc/info?id=11119',
    guideSteps: [
      { title: '进入“我的 API”', description: '确认当前应用拥有服务端应用能力。' },
      { title: '新建服务端应用', description: '按平台要求创建接口凭据。' },
      { title: '保存 client_id / client_secret', description: '这两个值对应这里的 Client ID / Client Secret。' },
    ],
    jumpLinkHint: 'OPPO 详情页链接请填写应用在软件商店的公开详情页地址，便于发布记录跳转。',
    guideHint: '字段与后端 submitToOppo 读取逻辑一致。',
    guideImage: oppoGuideImage,
  },
  {
    type: 'VIVO',
    label: 'vivo 应用商店',
    shortLabel: 'vivo',
    fields: [
      { key: 'accessKey', label: 'Access Key' },
      { key: 'accessSecret', label: 'Access Secret', type: 'password' },
      marketUrlField('vivo 应用商店详情页链接'),
    ],
    guideSubtitle: '在 API 管理中复制 access_key / access_secret',
    guideUrl: 'https://dev.vivo.com.cn/documentCenter/doc/326',
    guideSteps: [
      { title: '进入 api 管理', description: '找到当前应用对应的接口管理入口。' },
      { title: '激活后再读取密钥', description: '首次启用后可能需要刷新页面。' },
      { title: '填入 Access Key / Access Secret', description: '与后端提交服务字段保持一致。' },
    ],
    jumpLinkHint: 'vivo 应用商店详情页链接可直接从商店发布后的公开详情页复制。',
    guideHint: '提交服务读取 accessKey / accessSecret。',
    guideImage: vivoGuideImage,
  },
  {
    type: 'HONOR',
    label: '荣耀应用市场',
    shortLabel: '荣耀',
    fields: [
      { key: 'clientId', label: 'Client ID', placeholder: 'AppGallery Connect Client ID' },
      { key: 'clientSecret', label: 'Client Secret', type: 'password' },
      marketUrlField('荣耀应用市场详情页链接'),
    ],
    guideSubtitle: '在管理中心申请凭证并复制 Client_id / 密钥',
    guideUrl: 'https://developer.honor.com/cn',
    guideSteps: [
      { title: '进入管理中心', description: '打开荣耀开发者后台并进入凭证页。' },
      { title: '申请凭证', description: '创建用于服务端上传的 API 凭据。' },
      { title: '保存 Client_id / 密钥', description: '与这里的 Client ID / Client Secret 对应。' },
    ],
    jumpLinkHint: '荣耀应用的详情页链接请填写应用在荣耀市场的公开详情页地址。',
    guideHint: '与后端 Honor 提交流程完全一致；跳转页可选填写。',
    guideImage: honorGuideImage,
  },
  {
    type: 'APP_STORE',
    label: 'Apple App Store',
    shortLabel: 'App Store',
    fields: [
      { key: 'issuerId', label: 'Issuer ID', placeholder: 'App Store Connect → 用户和访问 → 整合 → Issuer ID' },
      { key: 'keyId', label: 'Key ID', placeholder: 'API 密钥页面的 Key ID' },
      { key: 'privateKey', label: '私钥 (.p8)', type: 'textarea', placeholder: '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----' },
      marketUrlField('App Store 详情页链接（可选）'),
    ],
    guideSubtitle: 'App Store Connect API Key 凭证',
    guideUrl: 'https://developer.apple.com/documentation/appstoreconnectapi',
    guideSteps: [
      { title: '创建 API Key', description: '进入 App Store Connect → 用户和访问 → 整合 → 点击 "+" 创建密钥。' },
      { title: '下载 .p8 私钥', description: '创建后立即下载私钥文件（只能下载一次），复制完整内容。' },
      { title: '填写 Issuer ID / Key ID / 私钥', description: 'Issuer ID 在整合页面顶部，Key ID 在密钥列表中。' },
    ],
    jumpLinkHint: 'App Store 链接可选填写，通常以 apps.apple.com 开头。可在 App Store Connect 的应用页面或 App Store 公开页面复制。',
    guideHint: '配置 API Key 后可自动查询审核状态。私钥为 .p8 文件的完整内容，包含 BEGIN/END 标记。',
  },
  {
    type: 'GOOGLE_PLAY',
    label: 'Google Play',
    shortLabel: 'Google Play',
    fields: [
      { key: 'serviceAccountJson', label: '服务账号 JSON', type: 'textarea', placeholder: '{ "type": "service_account", ... }' },
      marketUrlField('Google Play 详情页链接'),
    ],
    guideSubtitle: '使用服务账号 JSON 授权上传',
    guideUrl: 'https://developer.android.com/google/play/developer-api',
    guideSteps: [
      { title: '创建服务账号', description: '从 Google Cloud 获取服务账号 JSON。' },
      { title: '授予 Play 管理权限', description: '把该服务账号绑定到目标应用。' },
      { title: '粘贴 JSON 内容', description: '这里保存的是上传服务端所需内容。' },
    ],
    jumpLinkHint: 'Google Play 链接直接填写商店公开详情页即可。',
    guideHint: '目前字段以 JSON 方式保存，方便服务端直接读取。',
  },
  {
    type: 'HARMONY_APP',
    label: '鸿蒙应用',
    shortLabel: '鸿蒙',
    fields: [
      { key: 'clientId', label: 'Client ID', placeholder: 'AppGallery Connect Client ID' },
      { key: 'clientSecret', label: 'Client Secret', type: 'password', placeholder: 'AppGallery Connect Client Secret' },
      marketUrlField('鸿蒙应用市场详情页链接（可选）'),
    ],
    guideSubtitle: 'AppGallery Connect API 凭证',
    guideUrl: 'https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-Guides/agcapi-getstarted-0000001111845114',
    guideSteps: [
      { title: '进入 AppGallery Connect', description: '在华为开发者平台找到目标鸿蒙应用。' },
      { title: '创建 Connect API 凭据', description: '进入开发工具 → Connect API，创建服务端凭据。' },
      { title: '填写 Client ID / Client Secret', description: '复制凭据信息保存到此处。' },
    ],
    jumpLinkHint: '鸿蒙应用市场详情页链接可选填写，通常使用 appgallery.huawei.com/app/detail?id=包名。可在 AppGallery Connect 的应用页面复制。',
    guideHint: '配置 API 凭证后可自动查询审核状态。鸿蒙应用审核流程与华为应用市场共用 AppGallery Connect 平台。',
  },
  {
    type: 'REVIEW_WEBHOOK',
    label: '审核通知',
    shortLabel: '审核通知',
    fields: [
      { key: 'notifyType', label: '通知类型', type: 'select', options: [
        { label: '自定义（XuqmGroup 格式）', value: 'CUSTOM' },
        { label: '钉钉机器人', value: 'DINGTALK' },
        { label: '企业微信机器人', value: 'WECOM' },
        { label: '飞书机器人', value: 'FEISHU' },
      ]},
      { key: 'webhookUrl', label: 'Webhook 地址', placeholder: 'https://your.service/webhook 或机器人 Webhook 地址' },
      { key: 'secret', label: '签名密钥', type: 'password', placeholder: '可选，仅自定义格式使用',
        visibleWhen: (v) => !v.notifyType || v.notifyType === 'CUSTOM' },
    ],
    guideSubtitle: '所有市场审核状态共用这一套通知配置',
    guideUrl: 'https://cloud.tencent.com/document/product/269/32431',
    guideSteps: [
      { title: '配置回调地址', description: '填写你的业务系统回调 URL。' },
      { title: '配置签名密钥', description: '如需验签，可同步设置 secret。' },
      { title: '保存并启用', description: '后续任何市场审核状态变化都会复用这一个通知。' },
    ],
    jumpLinkHint: '不涉及应用市场跳转页，仅配置回调地址。',
    guideHint: '该配置对所有市场共用，不再按渠道重复配置。',
  },
]

const GUIDE_GROUPS = computed(() => [
  {
    label: 'Android 应用商店',
    stores: STORE_DEFS.filter(s => ['HUAWEI', 'MI', 'OPPO', 'VIVO', 'HONOR', 'GOOGLE_PLAY'].includes(s.type)),
  },
  {
    label: 'iOS / 鸿蒙',
    stores: STORE_DEFS.filter(s => ['APP_STORE', 'HARMONY_APP'].includes(s.type)),
  },
  {
    label: '通知配置',
    stores: STORE_DEFS.filter(s => s.type === 'REVIEW_WEBHOOK'),
  },
])

const guidePlatform = ref('android')
const guideExpandedStores = ref<string[]>([])
const guideFilteredStores = computed(() => {
  const map: Record<string, string[]> = {
    android: ['HUAWEI', 'MI', 'OPPO', 'VIVO', 'HONOR', 'GOOGLE_PLAY'],
    ios: ['APP_STORE'],
    harmony: ['HARMONY_APP'],
  }
  const types = map[guidePlatform.value] ?? []
  return STORE_DEFS.filter(s => types.includes(s.type))
})

function getStoreConfig(type: StoreType): StoreConfig | undefined {
  return storeConfigs.value.find(c => c.storeType === type)
}

function isStoreEnabled(type: StoreType): boolean {
  return getStoreConfig(type)?.enabled ?? false
}

const enabledStores = computed(() => STORE_DEFS.filter(s =>
  !['APP_STORE', 'HARMONY_APP', 'REVIEW_WEBHOOK'].includes(s.type) && isStoreEnabled(s.type),
))

const submissionStoreDefs = computed(() => STORE_DEFS.filter(s =>
  !['APP_STORE', 'HARMONY_APP', 'REVIEW_WEBHOOK'].includes(s.type) && isStoreEnabled(s.type),
))

async function toggleStore(type: StoreType, enabled: boolean) {
  const cfg = getStoreConfig(type)
  if (!cfg) return
  try {
    await updateAdminApi.saveStoreConfig(appKey.value, type, cfg.configJson ?? '{}', enabled)
    await loadStoreConfigs()
  } catch {
    ElMessage.error('操作失败')
  }
}

async function loadStoreConfigs() {
  try {
    const res = await updateAdminApi.getStoreConfigs(appKey.value)
    storeConfigs.value = res.data.data
  } catch {
    storeConfigs.value = []
  }
}

function switchApp(val: string) {
  saveLastServiceApp('update', val)
  router.push(`/services/update/${val}`)
}

async function loadApp() {
  const res = await appApi.get(appKey.value)
  app.value = res.data.data
}

const showStoreConfig = ref(false)
const savingStoreConfig = ref(false)
const currentStoreDef = ref<StoreDef | null>(null)
const storeConfigForm = ref<{ enabled: boolean; values: Record<string, string> }>({
  enabled: true,
  values: {},
})

function openStoreConfigDialog(store: StoreDef) {
  currentStoreDef.value = store
  const existing = getStoreConfig(store.type)
  let values: Record<string, string> = {}
  if (existing?.configJson) {
    try {
      values = JSON.parse(existing.configJson)
    } catch {
      values = {}
    }
  }
  storeConfigForm.value = { enabled: existing?.enabled ?? true, values }
  showStoreConfig.value = true
}

async function saveStoreConfig() {
  if (!currentStoreDef.value) return
  if (currentStoreDef.value.type === 'REVIEW_WEBHOOK') {
    const webhookUrl = storeConfigForm.value.values.webhookUrl?.trim() ?? ''
    if (!webhookUrl) {
      ElMessage.warning('请填写审核通知地址')
      return
    }
  }
  savingStoreConfig.value = true
  try {
    await updateAdminApi.saveStoreConfig(
      appKey.value,
      currentStoreDef.value.type,
      JSON.stringify(storeConfigForm.value.values),
      storeConfigForm.value.enabled,
    )
    ElMessage.success('凭据已保存')
    showStoreConfig.value = false
    await loadStoreConfigs()
  } catch {
    ElMessage.error('保存失败')
  } finally {
    savingStoreConfig.value = false
  }
}

async function removeStoreConfig(type: StoreType) {
  await ElMessageBox.confirm('确认删除此应用商店凭据？', '提示', { type: 'warning' })
  try {
    await updateAdminApi.deleteStoreConfig(appKey.value, type)
    ElMessage.success('已删除')
    await loadStoreConfigs()
  } catch {
    ElMessage.error('删除失败')
  }
}

function normalizePublishConfig(raw: Record<string, unknown> | null | undefined) {
  const normalizeUrl = (value: unknown) => {
    const text = String(value ?? '').trim()
    return /^https?:\/\//i.test(text) ? text : ''
  }
  return {
    allowAnonymousUpdateCheck: Boolean(raw?.allowAnonymousUpdateCheck ?? false),
    defaultGrayPercent: Number(raw?.defaultGrayPercent ?? 0),
    grayMode: String(raw?.grayMode ?? 'PERCENT') as GrayMode,
    graySyncUrl: normalizeUrl(raw?.graySyncUrl),
    publishCallbackUrl: normalizeUrl(raw?.publishCallbackUrl),
    enableRealtimeNotification: Boolean(raw?.enableRealtimeNotification ?? false),
    parallelStoreUpload: raw?.parallelStoreUpload !== false,
  }
}

function parsePublishConfig(config?: string | null) {
  if (!config) {
    return normalizePublishConfig({})
  }
  try {
    return normalizePublishConfig(JSON.parse(config) as Record<string, unknown>)
  } catch {
    return normalizePublishConfig({})
  }
}

async function loadPublishConfig() {
  loadingPublishConfig.value = true
  try {
    const res = await updateAdminApi.getPublishConfig(appKey.value)
    publishConfigForm.value = parsePublishConfig(res.data.data.configJson)
    if (allowAnonymousUpdateCheck.value) {
      publishConfigForm.value.grayMode = 'PERCENT'
    }
  } catch {
    publishConfigForm.value = normalizePublishConfig({})
  } finally {
    loadingPublishConfig.value = false
  }
}

async function savePublishConfig() {
  savingPublishConfig.value = true
  try {
    const normalizeUrl = (value: string) => {
      const trimmed = value.trim()
      return /^https?:\/\//i.test(trimmed) ? trimmed : ''
    }
    const payload = {
      ...publishConfigForm.value,
      defaultGrayPercent: Math.min(Math.max(Number(publishConfigForm.value.defaultGrayPercent || 0), 0), 100),
      graySyncUrl: normalizeUrl(publishConfigForm.value.graySyncUrl),
      publishCallbackUrl: normalizeUrl(publishConfigForm.value.publishCallbackUrl),
    }
    if (payload.allowAnonymousUpdateCheck) {
      payload.grayMode = 'PERCENT'
    }
    await updateAdminApi.savePublishConfig(appKey.value, payload)
    ElMessage.success('发布配置已保存')
  } catch {
    ElMessage.error('保存失败')
  } finally {
    savingPublishConfig.value = false
  }
}

const showSubmitStore = ref(false)
const submittingToStores = ref(false)
const submitStoreVersion = ref<AppVersion | null>(null)
const selectedStores = ref<StoreType[]>([])
const submitStoreMode = ref<PublishMode>('AUTO_REVIEW')
const submitStoreScheduledAt = ref('')

const showStoreReviewDetail = ref(false)
const storeReviewDetailVersion = ref<AppVersion | null>(null)
type StoreReviewItem = {
  store: string
  state: string
  reason?: string
  stage?: string
  submittedAt?: string
  updatedAt?: string
  batchId?: string
  liveOnStore?: boolean
  preExisting?: boolean
  currentSubmissionLive?: boolean
  nonCurrentRelease?: boolean
  liveVersionName?: string
  liveVersionCode?: string
  reviewVersionName?: string
  reviewVersionCode?: string
}
const storeReviewDetailItems = ref<StoreReviewItem[]>([])
const storeReviewDetailLive = ref(false)
const cancellingReview = ref(false)
const refreshingReviewStatus = ref(false)
const retryingStores = ref<Set<string>>(new Set())
const showPublishSchedule = ref(false)
const publishScheduleType = ref<'IMMEDIATE' | 'SCHEDULED'>('IMMEDIATE')
const publishScheduleAt = ref('')
const updatingPublishSchedule = ref(false)

const showChangelogEdit = ref(false)
const changelogEditId = ref('')
const changelogEditValue = ref('')
const savingChangelog = ref(false)

function openChangelogEdit(row: AppVersion) {
  changelogEditId.value = row.id
  changelogEditValue.value = row.changeLog ?? ''
  showChangelogEdit.value = true
}

async function submitChangelogEdit() {
  savingChangelog.value = true
  try {
    await updateAdminApi.patchChangeLog(changelogEditId.value, changelogEditValue.value)
    ElMessage.success('更新说明已保存')
    showChangelogEdit.value = false
    await loadAppVersions()
    await loadOperationLogs()
  } catch {
    ElMessage.error('保存失败')
  } finally {
    savingChangelog.value = false
  }
}

async function handleCancelReview(storeType?: string) {
  if (!storeReviewDetailVersion.value) return
  const stores = storeType
    ? [storeType]
    : storeReviewDetailItems.value.filter(i => isActiveState(i.state)).map(i => i.store)
  if (!stores.length) return
  cancellingReview.value = true
  try {
    await updateAdminApi.cancelStoreReview(storeReviewDetailVersion.value.id, stores as any)
    ElMessage.success('撤回请求已发送')
    // Optimistically update local state
    for (const s of stores) {
      const idx = storeReviewDetailItems.value.findIndex(i => i.store === s)
      if (idx >= 0) storeReviewDetailItems.value[idx] = { ...storeReviewDetailItems.value[idx], state: 'WITHDRAWN' }
    }
    await loadAppVersions()
  } catch {
    ElMessage.error('撤回失败，请稍后重试')
  } finally {
    cancellingReview.value = false
  }
}

async function handleRefreshReviewStatus() {
  if (!storeReviewDetailVersion.value) return
  refreshingReviewStatus.value = true
  try {
    const versionId = storeReviewDetailVersion.value.id
    await updateAdminApi.refreshStoreReviewStatus(versionId)
    await loadAppVersions()
    const row = appVersions.value.find(v => v.id === versionId)
    if (row) {
      storeReviewDetailVersion.value = row
      storeReviewDetailItems.value = parseStoreReview(row.storeReviewStatus)
    }
    ElMessage.success('厂商状态已刷新')
  } catch {
    ElMessage.error('刷新失败，请稍后重试')
  } finally {
    refreshingReviewStatus.value = false
  }
}

async function handleRetryStore(storeType: string) {
  if (!storeReviewDetailVersion.value) return
  retryingStores.value = new Set([...retryingStores.value, storeType])
  try {
    await updateAdminApi.executeSubmitToStores(
      storeReviewDetailVersion.value.id,
      [storeType] as any,
      storeReviewDetailVersion.value.storeSubmitMode ?? 'MANUAL',
    )
    ElMessage.success(`${storeLabel(storeType)} 重试已提交`)
    const idx = storeReviewDetailItems.value.findIndex(i => i.store === storeType)
    if (idx >= 0) storeReviewDetailItems.value[idx] = { ...storeReviewDetailItems.value[idx], state: 'PENDING', stage: 'QUEUED' }
    await loadAppVersions()
  } catch {
    ElMessage.error(`${storeLabel(storeType)} 重试失败，请稍后再试`)
  } finally {
    retryingStores.value = new Set([...retryingStores.value].filter(s => s !== storeType))
  }
}

async function handleUpdatePublishSchedule() {
  if (!storeReviewDetailVersion.value) return
  updatingPublishSchedule.value = true
  try {
    await updateAdminApi.updatePublishSchedule(
      storeReviewDetailVersion.value.id,
      publishScheduleType.value,
      publishScheduleType.value === 'SCHEDULED' ? publishScheduleAt.value : undefined,
    )
    ElMessage.success('发布计划已更新')
    showPublishSchedule.value = false
    await loadAppVersions()
  } catch {
    ElMessage.error('更新失败，请稍后重试')
  } finally {
    updatingPublishSchedule.value = false
  }
}

function openSubmitStoreDialog(row: AppVersion) {
  submitStoreVersion.value = row
  selectedStores.value = enabledStores.value
    .map(s => s.type)
    .filter(t => !isStoreActiveReview(row, t))
  const savedMode = row.storeSubmitMode
  submitStoreMode.value = (savedMode === 'SCHEDULED' ? 'SCHEDULED' : 'AUTO_REVIEW') as PublishMode
  submitStoreScheduledAt.value = row.storeSubmitScheduledAt ?? ''
  showSubmitStore.value = true
}

function getSubmitDialogStoreState(version: AppVersion | null, storeType: string): string {
  if (!version?.storeReviewStatus) return ''
  return parseStoreReview(version.storeReviewStatus).find(i => i.store === storeType)?.state ?? ''
}

function isStoreActiveReview(version: AppVersion | null, storeType: string): boolean {
  const state = getSubmitDialogStoreState(version, storeType)
  return state === 'UNDER_REVIEW' || state === 'APPROVED' || state === 'SUBMITTING'
}

function parseStoreTargets(json?: string) {
  if (!json) return []
  try {
    const value = JSON.parse(json)
    return Array.isArray(value) ? value.map(item => String(item)) : []
  } catch {
    return []
  }
}

function openStoreReviewDetail(row: AppVersion) {
  storeReviewDetailVersion.value = row
  storeReviewDetailItems.value = parseStoreReview(row.storeReviewStatus)
  storeReviewDetailLive.value = false
  showStoreReviewDetail.value = true
  startDialogPoll()
}

function startDialogPoll() {
  stopDialogPoll()
  // IM WebSocket delivers real-time store_review_update events; only fall back to
  // HTTP polling when IM has been silent for over 90 s (disconnected or starting up).
  const interval = isStoreReviewRealtimeConnected() ? 30_000 : 3_000
  storeReviewDialogPollTimer = setInterval(() => {
    if (!showStoreReviewDetail.value || !storeReviewDetailVersion.value) {
      stopDialogPoll()
      return
    }
    const hasActive = storeReviewDetailItems.value.some(i =>
      i.state === 'PENDING' || i.state === 'SUBMITTING' || i.state === 'UNDER_REVIEW'
    )
    if (hasActive) {
      void loadAppVersions()
    } else {
      stopDialogPoll()
    }
  }, interval)
}

function stopDialogPoll() {
  if (storeReviewDialogPollTimer) {
    clearInterval(storeReviewDialogPollTimer)
    storeReviewDialogPollTimer = null
  }
}

async function confirmSubmitToStores() {
  if (!submitStoreVersion.value || !selectedStores.value.length) {
    ElMessage.warning('请选择至少一个可用市场')
    return
  }
  if (submitStoreMode.value === 'SCHEDULED' && !submitStoreScheduledAt.value) {
    ElMessage.warning('请选择定时上架时间')
    return
  }
  submittingToStores.value = true
  try {
    await updateAdminApi.executeSubmitToStores(
      submitStoreVersion.value.id,
      selectedStores.value,
      submitStoreMode.value,
      submitStoreScheduledAt.value || undefined,
      submitStoreMode.value === 'AUTO_REVIEW',
    )
    ElMessage.success('已提交，服务端正在向应用市场上传，审核状态将通过 Webhook 或刷新页面查看')
    showSubmitStore.value = false
    await loadAppVersions()
    await loadOperationLogs()
  } catch {
    ElMessage.error('提交失败')
  } finally {
    submittingToStores.value = false
  }
}

const showGray = ref(false)
const submittingGray = ref(false)
const grayTarget = ref<{ id: string; type: 'app' | 'rn' } | null>(null)
const grayTags = ref<GrayTag[]>([])
const grayExtraInput = ref('')
const importingIm = ref(false)
const grayForm = ref({
  enabled: true,
  grayMode: 'PERCENT' as GrayMode,
  percent: 10,
  groupNames: [] as string[],
  extraMemberIds: [] as string[],
})

const grayTagMemberCount = computed(() => {
  const selected = new Set(grayForm.value.groupNames)
  return grayTags.value
    .filter(t => selected.has(t.tagName))
    .reduce((sum, t) => sum + t.memberCount, 0)
})

const grayOverlapCount = computed(() => {
  // 简化：无法精确计算标签与额外成员的重叠，显示为 0
  return 0
})

const grayTotalCount = computed(() => {
  return grayTagMemberCount.value + grayForm.value.extraMemberIds.length - grayOverlapCount.value
})

function addExtraMembers() {
  const input = grayExtraInput.value.trim()
  if (!input) return
  const ids = input.split(/[,，\s]+/).map(s => s.trim()).filter(Boolean)
  for (const id of ids) {
    if (!grayForm.value.extraMemberIds.includes(id)) {
      grayForm.value.extraMemberIds.push(id)
    }
  }
  grayExtraInput.value = ''
}

function removeExtraMember(uid: string) {
  grayForm.value.extraMemberIds = grayForm.value.extraMemberIds.filter(id => id !== uid)
}

function openGrayDialog(row: { id: string }, type: 'app' | 'rn') {
  if (allowAnonymousUpdateCheck.value) {
    ElMessage.warning('当前应用开启了免登录检查更新，灰度发布已禁用')
    return
  }
  grayTarget.value = { id: row.id, type }
  grayForm.value = {
    enabled: true,
    grayMode: 'PERCENT',
    percent: publishConfigForm.value.defaultGrayPercent || 10,
    groupNames: [],
    extraMemberIds: [],
  }
  grayExtraInput.value = ''
  showGray.value = true
  loadGrayTags()
}

async function loadGrayTags() {
  try {
    const res = await updateAdminApi.listGrayTags(appKey.value)
    grayTags.value = res.data.data
  } catch {
    grayTags.value = []
  }
}

async function importGrayFromIm() {
  importingIm.value = true
  try {
    const res = await updateAdminApi.importGrayMembersFromIm(appKey.value)
    const { added, updated, removed } = res.data.data
    ElMessage.success(`IM 导入完成：新增 ${added}，更新 ${updated}，移除 ${removed}`)
    await loadGrayTags()
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || 'IM 导入失败')
  } finally {
    importingIm.value = false
  }
}

async function submitGray() {
  if (!grayTarget.value) return
  if (allowAnonymousUpdateCheck.value) {
    ElMessage.warning('当前应用开启了免登录检查更新，灰度发布已禁用')
    return
  }
  if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS'
    && !grayForm.value.groupNames.length && !grayForm.value.extraMemberIds.length) {
    ElMessage.warning('请至少选择一个标签或指定一个成员')
    return
  }
  submittingGray.value = true
  try {
    const { id, type } = grayTarget.value
    const payload: {
      enabled: boolean
      grayMode: GrayMode
      percent?: number
      groupNames?: string[]
      extraMemberIds?: string[]
    } = {
      enabled: grayForm.value.enabled,
      grayMode: grayForm.value.grayMode,
    }
    if (grayForm.value.grayMode === 'PERCENT') {
      payload.percent = grayForm.value.percent
    } else {
      payload.groupNames = grayForm.value.groupNames
      payload.extraMemberIds = grayForm.value.extraMemberIds
    }
    if (type === 'app') {
      await updateAdminApi.grayAppVersion(id, payload)
      await loadAppVersions()
    } else {
      await updateAdminApi.grayRnBundle(id, payload)
      await loadRnBundles()
    }
    await loadOperationLogs()
    ElMessage.success('灰度配置已保存')
    showGray.value = false
  } finally {
    submittingGray.value = false
  }
}

const showUploadApp = ref(false)
const uploadingApp = ref(false)
const appUploadForm = ref({
  platform: 'ANDROID' as 'ANDROID' | 'IOS' | 'HARMONY',
  packageName: '',
  versionName: '',
  versionCode: 1,
  changeLog: '',
  appStoreUrl: '',
  marketUrl: '',
  file: null as File | null,
  fileUrl: '',
  fileSize: 0,
})

function getStoreJumpUrl(storeType: StoreType) {
  const cfg = storeConfigs.value.find(c => c.storeType === storeType)
  if (!cfg?.configJson) return ''
  try {
    const parsed = JSON.parse(cfg.configJson) as Record<string, unknown>
    const jump = String(parsed.marketUrl ?? '').trim()
    return jump
  } catch {
    return ''
  }
}

function openUploadAppDialog() {
  appUploadForm.value.platform = appPlatform.value
  appUploadForm.value.versionName = ''
  appUploadForm.value.versionCode = 1
  appUploadForm.value.changeLog = ''
  appUploadForm.value.file = null
  appUploadForm.value.fileUrl = ''
  appUploadForm.value.fileSize = 0
  appPackageUploadProgress.value = 0
  appPackageInspecting.value = false
  handleAppPlatformChange()
  showUploadApp.value = true
}

function handleAppPlatformChange() {
  if (appUploadForm.value.platform === 'ANDROID') {
    appUploadForm.value.packageName = app.value?.packageName ?? appUploadForm.value.packageName
    return
  }
  if (appUploadForm.value.platform === 'IOS') {
    appUploadForm.value.packageName = app.value?.packageName ?? appUploadForm.value.packageName
    appUploadForm.value.appStoreUrl = getStoreJumpUrl('APP_STORE') || appUploadForm.value.appStoreUrl
  }
  if (appUploadForm.value.platform === 'HARMONY') {
    appUploadForm.value.packageName = app.value?.packageName ?? appUploadForm.value.packageName
    appUploadForm.value.marketUrl = getStoreJumpUrl('HARMONY_APP') || appUploadForm.value.marketUrl
  }
  // iOS 和鸿蒙不需要上传文件，切换时清理 Android 遗留的文件状态
  appUploadForm.value.file = null
  appUploadForm.value.fileUrl = ''
  appUploadForm.value.fileSize = 0
  appPackageUploadProgress.value = 0
  appPackageInspecting.value = false
}

async function onAppPackageChange(uploadFile: { raw?: File } | null) {
  const file = uploadFile?.raw ?? null
  if (!file) return
  appUploadForm.value.file = file
  appUploadForm.value.fileUrl = ''
  appUploadForm.value.fileSize = 0

  appPackageInspecting.value = true
  appPackageUploadProgress.value = 0
  try {
    const uploaded = await fileApi.uploadFile(file, null, (percent) => {
      appPackageUploadProgress.value = percent
    })
    appPackageUploadProgress.value = 100
    const fileInfo = uploaded.data.data
    appUploadForm.value.fileUrl = fileInfo.url
    appUploadForm.value.fileSize = fileInfo.size
    const res = await updateAdminApi.inspectAppPackage(fileInfo.url)
    const inspected = res.data.data as AppPackageInspectResult
    const currentPackageName = app.value?.packageName?.trim() ?? ''
    if (currentPackageName && inspected.packageName && inspected.packageName !== currentPackageName) {
      try {
        await ElMessageBox.confirm(
          `当前应用包名是 ${currentPackageName}，你选择的文件包名是 ${inspected.packageName}。是否强制继续使用这个包？`,
          '包名不一致',
          {
            type: 'warning',
            confirmButtonText: '强制使用',
            cancelButtonText: '重新选择',
          },
        )
        appUploadForm.value.packageName = inspected.packageName
      } catch {
        appUploadForm.value.file = null
        appUploadForm.value.fileUrl = ''
        appUploadForm.value.fileSize = 0
        return
      }
    }
    if (inspected.platform) appUploadForm.value.platform = inspected.platform
    if (inspected.packageName) appUploadForm.value.packageName = inspected.packageName
    if (inspected.versionName) appUploadForm.value.versionName = inspected.versionName
    if (typeof inspected.versionCode === 'number') appUploadForm.value.versionCode = inspected.versionCode
  } catch {
    ElMessage.warning('已选择文件，但未能完整识别，请补全版本信息后上传')
  } finally {
    appPackageUploadProgress.value = 0
    appPackageInspecting.value = false
  }
}

function removeUploadedFile() {
  appUploadForm.value.file = null
  appUploadForm.value.fileUrl = ''
  appUploadForm.value.fileSize = 0
  if (appUploadRef.value) {
    appUploadRef.value.clearFiles?.()
  }
}

async function submitAppUpload() {
  const f = appUploadForm.value
  if (f.platform === 'ANDROID' && !f.fileUrl) return ElMessage.warning('请先选择 Android 安装包文件')
  if (!f.versionName || !f.versionCode) return ElMessage.warning('请填写版本信息')

  uploadingApp.value = true
  appVersionUploadProgress.value = 0
  try {
    const fd = new FormData()
    fd.append('appKey', appKey.value)
    fd.append('platform', f.platform)
    fd.append('versionName', f.versionName)
    fd.append('versionCode', String(f.versionCode))
    if (f.packageName) fd.append('packageName', f.packageName)
    if (f.changeLog) fd.append('changeLog', f.changeLog)
    if (f.appStoreUrl) fd.append('appStoreUrl', f.appStoreUrl)
    if (f.marketUrl) fd.append('marketUrl', f.marketUrl)
    if (f.platform === 'ANDROID' && f.fileUrl) {
      fd.append('apkUrl', f.fileUrl)
      fd.append('apkSize', String(f.fileSize))
    }
    await updateAdminApi.uploadAppVersion(fd, (percent) => {
      appVersionUploadProgress.value = percent
    })
    appVersionUploadProgress.value = 100
    ElMessage.success('上传成功')
    showUploadApp.value = false
    await loadAppVersions()
  } finally {
    appVersionUploadProgress.value = 0
    uploadingApp.value = false
  }
}

const showUploadRn = ref(false)
const uploadingRn = ref(false)
const rnUploadForm = ref({
  note: '',
  file: null as File | null,
})

async function onRnBundleChange(uploadFile: { raw?: File } | null) {
  const file = uploadFile?.raw ?? null
  rnUploadForm.value.file = file
  if (!file) return
  if (!file.name.toLowerCase().endsWith('.xuqm.zip')) {
    rnUploadForm.value.file = null
    ElMessage.warning('RN 插件只接受发布 CLI 生成的 .xuqm.zip')
  }
}

async function submitRnUpload() {
  const f = rnUploadForm.value
  if (!f.file) return ElMessage.warning('请选择 CLI 生成的 .xuqm.zip 发布包')
  uploadingRn.value = true
  rnBundleUploadProgress.value = 0
  try {
    const fd = new FormData()
    fd.append('appKey', appKey.value)
    if (f.note) fd.append('note', f.note)
    fd.append('bundle', f.file)
    await updateAdminApi.uploadRnBundle(fd, (percent) => {
      rnBundleUploadProgress.value = percent
    })
    rnBundleUploadProgress.value = 100
    ElMessage.success('Bundle 上传成功')
    showUploadRn.value = false
    await loadRnBundles()
  } finally {
    rnBundleUploadProgress.value = 0
    uploadingRn.value = false
  }
}

async function loadAppVersions() {
  loadingApp.value = true
  try {
    const res = await updateAdminApi.listAppVersions(appKey.value, appPlatform.value)
    appVersions.value = res.data.data
    if (showStoreReviewDetail.value && storeReviewDetailVersion.value) {
      const updated = appVersions.value.find(v => v.id === storeReviewDetailVersion.value!.id)
      if (updated) {
        storeReviewDetailVersion.value = updated
        storeReviewDetailItems.value = parseStoreReview(updated.storeReviewStatus)
      }
    }
  } catch {
    ElMessage.error('加载失败')
  } finally {
    loadingApp.value = false
  }
}

async function loadRnBundles() {
  loadingRn.value = true
  try {
    const res = await updateAdminApi.listRnBundles(appKey.value, rnModuleFilter.value || undefined, rnPlatform.value || undefined)
    rnBundles.value = res.data.data
  } catch {
    ElMessage.error('加载失败')
  } finally {
    loadingRn.value = false
  }
}

const showPublish = ref(false)
const submittingPublish = ref(false)
const publishTarget = ref<{ id: string; type: 'app' | 'rn'; row?: AppVersion | RnBundle } | null>(null)
const publishForm = ref({
  publishImmediately: true,
  scheduledPublishAt: '',
  forceUpdate: false,
})

function openPublishDialog(row: AppVersion | RnBundle, type: 'app' | 'rn') {
  publishTarget.value = { id: row.id, type, row }
  publishForm.value = {
    publishImmediately: !row.scheduledPublishAt,
    scheduledPublishAt: row.scheduledPublishAt ?? '',
    forceUpdate: 'forceUpdate' in row ? Boolean((row as AppVersion).forceUpdate) : false,
  }
  showPublish.value = true
}

async function submitPublish() {
  if (!publishTarget.value) return
  if (!publishForm.value.publishImmediately && !publishForm.value.scheduledPublishAt) {
    ElMessage.warning('请选择计划发布时间')
    return
  }
  submittingPublish.value = true
  try {
    const body = {
      publishImmediately: publishForm.value.publishImmediately,
      scheduledPublishAt: publishForm.value.scheduledPublishAt || undefined,
      forceUpdate: publishForm.value.forceUpdate,
    }
    if (publishTarget.value.type === 'app') {
      await updateAdminApi.publishAppVersion(publishTarget.value.id, body)
      await loadAppVersions()
    } else {
      await updateAdminApi.publishRnBundle(publishTarget.value.id, body)
      await loadRnBundles()
    }
    await loadOperationLogs()
    ElMessage.success('已保存发布操作')
    showPublish.value = false
  } catch {
    ElMessage.error('发布失败')
  } finally {
    submittingPublish.value = false
  }
}

async function promptUnpublishReason(title: string) {
  let value = ''
  try {
    await ElMessageBox.confirm('下架后该版本将不再作为可发布版本，请确认继续。', title, {
      confirmButtonText: '下一步',
      cancelButtonText: '取消',
      type: 'warning',
    })
  } catch {
    return null
  }
  try {
    ({ value } = await ElMessageBox.prompt('请输入下架原因', title, {
      confirmButtonText: '下架',
      cancelButtonText: '取消',
      inputPlaceholder: '请填写下架原因',
      inputValidator: (input: string) => Boolean(input && input.trim()),
      inputErrorMessage: '请填写下架原因',
      type: 'warning',
    }))
  } catch {
    return null
  }
  return value
}

async function promptUnpublishApp(id: string) {
  const reason = await promptUnpublishReason('下架确认')
  if (!reason) return
  try {
    await updateAdminApi.unpublishAppVersion(id, reason)
    ElMessage.success('已下架')
    await loadAppVersions()
    await loadOperationLogs()
  } catch {
    ElMessage.error('下架失败')
  }
}

async function promptDeleteAppVersion(row: AppVersion) {
  try {
    await ElMessageBox.confirm(
      `确定删除版本 ${row.versionName}（${row.versionCode}）吗？删除后不可恢复。`,
      '删除确认',
      { confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' },
    )
    await updateAdminApi.deleteAppVersion(row.id)
    ElMessage.success('已删除')
    await loadAppVersions()
    await loadOperationLogs()
  } catch (e: any) {
    if (e !== 'cancel') {
      ElMessage.error(e?.response?.data?.message || '删除失败')
    }
  }
}

function hasActiveStoreReview(row: AppVersion): boolean {
  if (!row.storeReviewStatus) return false
  const items = parseStoreReview(row.storeReviewStatus)
  return items.some(i => i.state === 'SUBMITTING' || i.state === 'UNDER_REVIEW')
}

async function promptUnpublishRn(id: string) {
  const reason = await promptUnpublishReason('下架确认')
  if (!reason) return
  try {
    await updateAdminApi.unpublishRnBundle(id, reason)
    ElMessage.success('已下架')
    await loadRnBundles()
    await loadOperationLogs()
  } catch {
    ElMessage.error('下架失败')
  }
}


function statusLabel(row: { publishStatus: string }) {
  return { DRAFT: '草稿', PUBLISHED: '已发布', DEPRECATED: '已下架' }[row.publishStatus] ?? row.publishStatus
}

function statusTagType(row: { publishStatus: string }) {
  return { DRAFT: '', PUBLISHED: 'success', DEPRECATED: 'info' }[row.publishStatus] ?? ''
}

function storeLabel(type: string) {
  return STORE_DEFS.find(s => s.type === type)?.label ?? type
}

function reviewLabel(state: string): string {
  return {
    PENDING: '待提交', SUBMITTING: '提交中', UNDER_REVIEW: '审核中',
    APPROVED: '已上线', REJECTED: '已拒绝', WITHDRAWN: '已撤回', FAILED: '提交失败',
  }[state] ?? state
}

function reviewDisplayLabel(item: StoreReviewItem): string {
  if (item.state === 'APPROVED' && (item.nonCurrentRelease || item.currentSubmissionLive === false)) {
    return '已上线（非本次发布）'
  }
  return reviewLabel(item.state)
}

function reviewTagType(state: string): string {
  return {
    PENDING: 'info', SUBMITTING: 'primary', UNDER_REVIEW: 'warning',
    APPROVED: 'success', REJECTED: 'danger', WITHDRAWN: '', FAILED: 'danger',
  }[state] ?? ''
}

function isActiveState(state: string) {
  return state === 'PENDING' || state === 'SUBMITTING' || state === 'UNDER_REVIEW'
}

// Step definitions for per-store progress
const reviewSteps = [
  { stage: 'QUEUED',     label: '排队' },
  { stage: 'SUBMITTING', label: '上传' },
  { stage: 'SUBMITTED',  label: '审核中' },
  { stage: 'DONE',       label: '完成' },
]

const STAGE_ORDER: Record<string, number> = {
  QUEUED: 0, SUBMITTING: 1, SUBMITTED: 2,
  APPROVED: 3, FAILED: 3, WITHDRAWN: 3, DONE: 3,
}

function stageIndex(state: string, stage: string): number {
  if (state === 'APPROVED' || state === 'REJECTED' || state === 'WITHDRAWN') return 3
  if (stage) return STAGE_ORDER[stage] ?? 0
  return { PENDING: 0, SUBMITTING: 1, UNDER_REVIEW: 2 }[state] ?? 0
}

function reviewStepDone(state: string, step: { stage: string }): boolean {
  if (step.stage === 'DONE') return state === 'APPROVED'
  const si = stageIndex(state, '')
  const ti = STAGE_ORDER[step.stage] ?? 0
  if (state === 'APPROVED') return true
  if (state === 'REJECTED') return ti < 3
  if (state === 'WITHDRAWN') return false
  return ti < si
}

function reviewStepActive(state: string, stage: string, step: { stage: string }): boolean {
  if (step.stage === 'DONE') return false
  const cur = stageIndex(state, stage)
  const ti = STAGE_ORDER[step.stage] ?? 0
  return ti === cur && isActiveState(state)
}

function reviewStepClass(state: string, stage: string, step: { stage: string }): Record<string, boolean> {
  return {
    'step--done': reviewStepDone(state, step),
    'step--active': reviewStepActive(state, stage, step),
    'step--failed': step.stage === 'DONE' && state === 'REJECTED',
    'step--withdrawn': step.stage === 'DONE' && state === 'WITHDRAWN',
  }
}

function operationResourceLabel(resourceType: string) {
  return {
    APP_VERSION: 'App 版本',
    RN_BUNDLE: 'RN Bundle',
  }[resourceType] ?? resourceType
}

function operationActionLabel(action: string) {
  return {
    UPLOAD: '上传',
    PUBLISH: '发布',
    REPUBLISH: '重新上架',
    SCHEDULE_PUBLISH: '定时发布',
    UPDATE_FORCE: '修改强更',
    SAVE_DRAFT: '保存草稿',
    UNPUBLISH: '下架',
    STORE_SUBMIT: '提交市场',
    STORE_SUBMIT_REQUEST: '提交市场请求',
    STORE_SUBMIT_BATCH_START: '市场提交开始',
    STORE_SUBMIT_BATCH_END: '市场提交结束',
    STORE_SUBMIT_BATCH_FAILED: '市场提交失败',
    STORE_SUBMIT_BATCH_SKIPPED: '市场提交跳过',
    STORE_SUBMIT_STORE_START: '市场提交开始',
    STORE_SUBMIT_STORE_STAGE: '市场提交阶段',
    STORE_SUBMIT_STORE_SUCCESS: '市场提交成功',
    STORE_SUBMIT_STORE_FAILED: '市场提交失败',
    STORE_REVIEW: '审核回写',
    GRAY_UPDATE: '灰度配置',
    AUTO_PUBLISH: '自动发布',
    CREATE_STORE_CONFIG: '创建商店配置',
    UPDATE_STORE_CONFIG: '更新商店配置',
    DELETE_STORE_CONFIG: '删除商店配置',
  }[action] ?? action
}

function formatDetail(detailJson?: string) {
  if (!detailJson) return '-'
  try {
    const value = JSON.parse(detailJson) as Record<string, unknown>
    return Object.entries(value)
      .map(([key, val]) => `${key}: ${Array.isArray(val) ? val.join(', ') : String(val)}`)
      .join('；')
  } catch {
    return detailJson
  }
}

function updateViewport() {
  isMobile.value = window.innerWidth < 768
}

async function loadOperationLogs() {
  loadingOperationLogs.value = true
  try {
    const res = await updateAdminApi.listOperationLogs(appKey.value, 100)
    operationLogs.value = res.data.data
  } catch {
    operationLogs.value = []
  } finally {
    loadingOperationLogs.value = false
  }
}

async function loadStats() {
  if (!appKey.value) return
  loadingStats.value = true
  try {
    const [versionRes, deviceRes, trendRes] = await Promise.all([
      updateAdminApi.getVersionDistribution(appKey.value, statsPlatform.value, statsDays.value),
      updateAdminApi.getDeviceDistribution(appKey.value, statsPlatform.value, statsDays.value),
      updateAdminApi.getDailyTrend(appKey.value, statsPlatform.value, statsDays.value),
    ])
    statsVersionData.value = versionRes.data.data
    statsModelData.value = deviceRes.data.data.models
    statsOsData.value = deviceRes.data.data.osVersions
    statsVendorData.value = deviceRes.data.data.vendors
    statsTrendData.value = trendRes.data.data
    await nextTick()
    renderCharts()
  } catch {
    statsVersionData.value = []
    statsTrendData.value = []
    statsModelData.value = []
    statsOsData.value = []
    statsVendorData.value = []
  } finally {
    loadingStats.value = false
  }
}

function handleChartResize() {
  versionChart?.resize()
  trendChart?.resize()
  modelChart?.resize()
  osChart?.resize()
  vendorChart?.resize()
}

function renderCharts() {
  // Version distribution pie chart
  if (versionChartRef.value) {
    if (!versionChart) versionChart = echarts.init(versionChartRef.value)
    versionChart.setOption({
      tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
      legend: { orient: 'vertical', right: 10, top: 'center' },
      series: [{
        type: 'pie',
        radius: ['40%', '70%'],
        avoidLabelOverlap: false,
        itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
        label: { show: false },
        emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
        data: statsVersionData.value.map(d => ({
          name: d.versionName || `v${d.versionCode}`,
          value: d.deviceCount,
        })),
      }],
    })
  }

  // Daily trend line chart
  if (trendChartRef.value) {
    if (!trendChart) trendChart = echarts.init(trendChartRef.value)
    trendChart.setOption({
      tooltip: { trigger: 'axis' },
      xAxis: { type: 'category', data: statsTrendData.value.map(d => d.day) },
      yAxis: { type: 'value', minInterval: 1 },
      series: [{
        type: 'line',
        smooth: true,
        data: statsTrendData.value.map(d => d.deviceCount),
        areaStyle: { opacity: 0.15 },
      }],
    })
  }

  // Model distribution bar chart
  if (modelChartRef.value) {
    if (!modelChart) modelChart = echarts.init(modelChartRef.value)
    const modelData = [...statsModelData.value].reverse()
    modelChart.setOption({
      tooltip: { trigger: 'axis' },
      xAxis: { type: 'value', minInterval: 1 },
      yAxis: { type: 'category', data: modelData.map(d => d.model), axisLabel: { width: 120, overflow: 'truncate' } },
      series: [{ type: 'bar', data: modelData.map(d => d.deviceCount) }],
    })
  }

  // OS version pie chart
  if (osChartRef.value) {
    if (!osChart) osChart = echarts.init(osChartRef.value)
    osChart.setOption({
      tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
      legend: { orient: 'vertical', right: 10, top: 'center' },
      series: [{
        type: 'pie',
        radius: ['40%', '70%'],
        avoidLabelOverlap: false,
        itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
        label: { show: false },
        emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
        data: statsOsData.value.map(d => ({ name: d.osVersion, value: d.deviceCount })),
      }],
    })
  }

  // Vendor distribution pie chart
  if (vendorChartRef.value) {
    if (!vendorChart) vendorChart = echarts.init(vendorChartRef.value)
    vendorChart.setOption({
      tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
      legend: { orient: 'vertical', right: 10, top: 'center' },
      series: [{
        type: 'pie',
        radius: ['40%', '70%'],
        avoidLabelOverlap: false,
        itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
        label: { show: false },
        emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
        data: statsVendorData.value.map(d => ({ name: d.vendor, value: d.deviceCount })),
      }],
    })
  }
}

function scheduleStoreReviewReload() {
  if (storeReviewReloadTimer) {
    clearTimeout(storeReviewReloadTimer)
  }
  storeReviewReloadTimer = setTimeout(() => {
    void loadAppVersions()
    void loadOperationLogs()
  }, 200)
}

function parseStoreReview(json?: string): StoreReviewItem[] {
  if (!json) return []
  try {
    const m = JSON.parse(json) as Record<string, unknown>
    return Object.entries(m).map(([store, value]) => {
      if (typeof value === 'string') {
        return { store, state: value, reason: '' }
      }
      if (value && typeof value === 'object') {
        const item = value as Record<string, unknown>
        return {
          store,
          state: String(item.state ?? ''),
          reason: String(item.reason ?? ''),
          stage: String(item.stage ?? ''),
          submittedAt: String(item.submittedAt ?? ''),
          updatedAt: String(item.updatedAt ?? ''),
          batchId: String(item.batchId ?? ''),
          liveOnStore: item.liveOnStore === true,
          preExisting: item.preExisting === true,
          currentSubmissionLive: typeof item.currentSubmissionLive === 'boolean' ? item.currentSubmissionLive : undefined,
          nonCurrentRelease: item.nonCurrentRelease === true,
          liveVersionName: String(item.liveVersionName ?? ''),
          liveVersionCode: String(item.liveVersionCode ?? ''),
          reviewVersionName: String(item.reviewVersionName ?? ''),
          reviewVersionCode: String(item.reviewVersionCode ?? ''),
        }
      }
      return { store, state: String(value ?? ''), reason: '' }
    })
  } catch {
    return []
  }
}

watch(appKey, (key) => {
  if (isServicesPortal.value && key) checkServiceEnabled()
})

watch(activeTab, (tab) => {
  if (tab === 'stats') {
    loadStats()
  }
})

watch(app, (value) => {
  if (value?.packageName) {
    appUploadForm.value.packageName = value.packageName
  }
})

watch(storeConfigs, () => {
  if (appUploadForm.value.platform === 'IOS') {
    appUploadForm.value.appStoreUrl = getStoreJumpUrl('APP_STORE') || appUploadForm.value.appStoreUrl
  }
  if (appUploadForm.value.platform === 'HARMONY') {
    appUploadForm.value.marketUrl = getStoreJumpUrl('HARMONY_APP') || appUploadForm.value.marketUrl
  }
}, { deep: true })

watch(grayForm, () => {
  if (showGray.value && grayForm.value.grayMode === 'MEMBERS') {
    loadGrayTags()
  }
}, { deep: true })

onMounted(() => {
  updateViewport()
  window.addEventListener('resize', updateViewport)
  window.addEventListener('resize', handleChartResize)
  if (isServicesPortal.value) {
    appApi.list().then(res => {
      portalApps.value = res.data.data
      if (!appKey.value && portalApps.value.length) {
        const saved = getLastServiceApp('update')
        const target = saved && portalApps.value.some(a => a.appKey === saved) ? saved : portalApps.value[0].appKey
        switchApp(target)
      }
    })
    if (!appKey.value) return
    checkServiceEnabled()
  }
  loadApp()
  loadAppVersions()
  loadRnBundles()
  loadStoreConfigs()
  loadPublishConfig()
  loadOperationLogs()
  void connectStoreReviewRealtime(appKey.value, (event: StoreReviewRefreshEvent) => {
    const storeName = storeLabel(event.storeType || '') || event.storeType || '应用市场'
    const stateName = reviewLabel((event.reviewState || '').toUpperCase()) || event.reviewState || '状态变更'

    // Patch the open dialog card directly for real-time feel
    const openVersionId = storeReviewDetailVersion.value?.id
    if (showStoreReviewDetail.value && openVersionId && openVersionId === event.versionId && event.storeType) {
      storeReviewDetailLive.value = true
      const idx = storeReviewDetailItems.value.findIndex(i => i.store === event.storeType)
      const patch = {
        store: event.storeType,
        state: (event.reviewState || '').toUpperCase(),
        stage: event.stage || '',
        reason: event.reviewReason || '',
        batchId: event.batchId || storeReviewDetailItems.value[idx]?.batchId || '',
        submittedAt: storeReviewDetailItems.value[idx]?.submittedAt || new Date().toISOString(),
        updatedAt: new Date().toISOString(),
      }
      if (idx >= 0) {
        storeReviewDetailItems.value[idx] = patch
      } else {
        storeReviewDetailItems.value.push(patch)
      }
    }

    // Toast: show for all state/stage changes except PENDING (initial queue)
    const state = (event.reviewState || '').toUpperCase()
    const stage = (event.stage || '').toUpperCase()
    if (state && state !== 'PENDING') {
      // Use stage description for SUBMITTING transitions (排队→上传→审核中)
      let stageHint = ''
      if (state === 'SUBMITTING') {
        stageHint = stage === 'QUEUED' ? '排队中' : stage === 'SUBMITTING' ? '正在上传' : ''
      }
      ElMessage({
        message: event.reviewReason
          ? `${storeName} → ${stageHint || stateName}：${event.reviewReason}`
          : `${storeName} → ${stageHint || stateName}`,
        type: state === 'REJECTED' || state === 'WITHDRAWN' ? 'error'
            : state === 'APPROVED' ? 'success' : 'info',
        duration: state === 'SUBMITTING' ? 2000 : 4000,
      })
    }

    scheduleStoreReviewReload()
  }).catch((error) => {
    if (import.meta.env.DEV) {
      console.warn('[tenant-platform] store review realtime unavailable', error)
    }
  })

  // Fallback: poll every 30s while any store is in an active state so WebSocket
  // drop-outs don't leave the UI stale (e.g. a 20-min upload where WS reconnects).
  storeReviewAutoRefreshTimer = setInterval(() => {
    const hasActive = appVersions.value.some(v => {
      if (!v.storeReviewStatus) return false
      try {
        const m = JSON.parse(v.storeReviewStatus) as Record<string, unknown>
        return Object.values(m).some(entry => {
          const state = typeof entry === 'string' ? entry : (entry as Record<string, unknown>)?.state as string
          return state === 'PENDING' || state === 'SUBMITTING' || state === 'UNDER_REVIEW'
        })
      } catch { return false }
    })
    if (hasActive) {
      void loadAppVersions()
    }
  }, 30_000)
})

onBeforeUnmount(() => {
  window.removeEventListener('resize', updateViewport)
  window.removeEventListener('resize', handleChartResize)
  disconnectStoreReviewRealtime()
  if (storeReviewReloadTimer) {
    clearTimeout(storeReviewReloadTimer)
    storeReviewReloadTimer = null
  }
  if (storeReviewAutoRefreshTimer) {
    clearInterval(storeReviewAutoRefreshTimer)
    storeReviewAutoRefreshTimer = null
  }
  stopDialogPoll()
  versionChart?.dispose()
  trendChart?.dispose()
  modelChart?.dispose()
  osChart?.dispose()
  vendorChart?.dispose()
})
</script>

<style scoped>
.toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
  flex-wrap: wrap;
}
.responsive-toolbar {
  justify-content: space-between;
}
.table-wrap {
  overflow-x: auto;
}
.text-muted { color: var(--el-text-color-placeholder); font-size: 12px; }
.form-tip { font-size: 12px; color: var(--el-text-color-secondary); margin-left: 8px; }

/* Store config grid */
.store-upload-options {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  padding: 10px 14px;
  background: var(--el-fill-color-light);
  border-radius: 6px;
}
.store-option-label { font-weight: 600; font-size: 14px; white-space: nowrap; }
.store-option-hint { font-size: 12px; color: var(--el-text-color-secondary); }

.store-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}
.store-card { cursor: default; }
.store-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
}
.store-card-name { font-weight: 600; font-size: 14px; }
.store-card-status { margin-bottom: 12px; }
.store-card-footer { display: flex; gap: 8px; }

/* Submit store checkbox */
.store-checkbox-row { padding: 6px 0; }

.store-config-form {
  min-width: 0;
}

.release-config-form {
  max-width: 920px;
}

.guide-platform-tabs {
  margin-bottom: 20px;
}

.guide-store-list {
  max-width: 800px;
}

.guide-store-header {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
}

.guide-store-name {
  font-weight: 600;
  font-size: 14px;
}

.guide-store-body {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 4px 0;
}

.guide-store-steps {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.guide-step-row {
  display: flex;
  gap: 10px;
}

.guide-step-badge {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--el-color-primary);
  color: #fff;
  font-size: 12px;
  font-weight: 600;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  margin-top: 2px;
}

.guide-step-title {
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 1px;
}

.guide-step-desc {
  font-size: 12px;
  color: var(--el-text-color-regular);
  line-height: 1.5;
}

.guide-store-image {
  width: 100%;
  max-height: 200px;
  border-radius: 6px;
}

.guide-store-hint {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  line-height: 1.6;
  padding: 10px 12px;
  background: var(--el-fill-color-light);
  border-radius: 6px;
}

.guide-store-link {
  padding-top: 12px;
  border-top: 1px solid var(--el-border-color-lighter);
}

.guide-store-link-label {
  font-weight: 600;
  font-size: 12px;
  margin-bottom: 4px;
}

.guide-store-link-hint {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  line-height: 1.5;
  margin-bottom: 8px;
}

.guide-card {
  min-height: 100%;
}

.guide-card-title-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 8px;
}

.guide-card-title {
  font-weight: 600;
  font-size: 14px;
}

.guide-card-subtitle {
  font-size: 12px;
  color: var(--el-text-color-secondary);
  margin-top: 2px;
}

.guide-image {
  width: 100%;
  height: 160px;
  margin: 8px 0 12px;
  border-radius: 6px;
}

.guide-link-title {
  font-weight: 600;
  font-size: 13px;
  margin-bottom: 4px;
}

.guide-link-hint {
  color: var(--el-text-color-secondary);
  font-size: 13px;
  line-height: 1.6;
  margin-bottom: 8px;
}

.guide-hint {
  color: var(--el-text-color-secondary);
  font-size: 13px;
  line-height: 1.6;
}

.apk-dropzone {
  width: 100%;
}

.apk-dropzone :deep(.el-upload) {
  width: 100%;
}

.apk-dropzone :deep(.el-upload-dragger) {
  width: 100%;
  min-height: 180px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  border-radius: 16px;
}

.apk-dropzone :deep(.el-upload__text) {
  margin-top: 12px;
  color: var(--el-text-color-primary);
}

.apk-dropzone :deep(.el-upload__tip) {
  text-align: center;
  color: var(--el-text-color-secondary);
}

.bundle-dropzone {
  width: 100%;
}

.bundle-dropzone :deep(.el-upload) {
  width: 100%;
}

.bundle-dropzone :deep(.el-upload-dragger) {
  width: 100%;
  min-height: 180px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  border-radius: 16px;
}

.bundle-dropzone :deep(.el-upload__text) {
  margin-top: 12px;
  color: var(--el-text-color-primary);
}

.bundle-dropzone :deep(.el-upload__tip) {
  text-align: center;
  color: var(--el-text-color-secondary);
}

.gray-tag-item {
  padding: 6px 0;
  border-bottom: 1px solid var(--el-border-color-lighter);
}
.gray-tag-item:last-child {
  border-bottom: none;
}

.gray-extra-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

.gray-summary {
  color: var(--el-text-color-secondary);
  font-size: 13px;
}
.gray-summary strong {
  color: var(--el-color-primary);
  font-size: 16px;
}

.upload-progress-block {
  display: flex;
  flex-direction: column;
  gap: 6px;
  width: 100%;
}

.upload-progress-text {
  font-size: 12px;
  color: var(--el-text-color-secondary);
}

.release-store-checkbox-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

@media (max-width: 767px) {
  .responsive-toolbar {
    align-items: stretch;
  }

  .responsive-toolbar :deep(.el-radio-group),
  .responsive-toolbar :deep(.el-button) {
    width: 100%;
  }

  .responsive-toolbar :deep(.el-radio-group) {
    display: flex;
    flex-wrap: wrap;
  }

  .responsive-toolbar :deep(.el-radio-button) {
    flex: 1 1 32%;
  }

  .table-wrap :deep(.el-table) {
    min-width: 980px;
  }

  .store-review-cell {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }

  .store-review-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
  }

  .store-review-detail-btn {
    align-self: flex-start;
    padding: 0;
    min-height: 20px;
  }

  .store-grid,
  .guide-grid {
    grid-template-columns: 1fr;
  }

  .release-config-form :deep(.el-form-item__content) {
    min-width: 0;
  }

  .release-store-checkbox-row {
    flex-direction: column;
    align-items: flex-start;
  }

  .release-store-checkbox-row :deep(.el-tag) {
    margin-left: 0 !important;
  }

  :deep(.el-dialog) {
    margin: 8px auto;
  }
}

.portal-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 24px;
}
.portal-bar-title {
  font-size: 16px;
  font-weight: 600;
}

.drag-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  z-index: 3000;
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(2px);
}

.drag-overlay-content {
  background: var(--el-bg-color);
  border-radius: 16px;
  padding: 48px 64px;
  text-align: center;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
}

.drag-overlay-title {
  margin-top: 16px;
  font-size: 18px;
  font-weight: 600;
  color: var(--el-text-color-primary);
}

.drag-overlay-hint {
  margin-top: 8px;
  font-size: 13px;
  color: var(--el-text-color-secondary);
}

/* ── Store review card dialog ─────────────────────────────────── */
.review-detail-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 0 16px;
  border-bottom: 1px solid var(--el-border-color-lighter);
  margin-bottom: 16px;
  flex-wrap: wrap;
}

.review-detail-version {
  font-size: 15px;
  font-weight: 600;
  color: var(--el-text-color-primary);
}

.review-detail-code {
  font-size: 13px;
  color: var(--el-text-color-secondary);
  margin-left: 4px;
}

.review-live-dot {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  color: var(--el-color-success);
  margin-left: auto;
}

.live-pulse {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--el-color-success);
  animation: live-blink 1.2s ease-in-out infinite;
}

@keyframes live-blink {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.2; }
}

.store-review-cards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
}

.review-store-card {
  border: 1px solid var(--el-border-color);
  border-radius: 8px;
  padding: 14px;
  background: var(--el-bg-color);
  transition: box-shadow 0.2s, border-color 0.2s;
}

.review-store-card.review-card--active {
  border-color: var(--el-color-primary-light-5);
  box-shadow: 0 0 0 2px var(--el-color-primary-light-8);
}

.review-store-card.review-card--approved {
  border-color: var(--el-color-success-light-5);
}

.review-store-card.review-card--rejected,
.review-store-card.review-card--withdrawn {
  border-color: var(--el-border-color-light);
  opacity: 0.85;
}

.review-store-card.review-card--failed {
  border-color: var(--el-color-danger-light-5);
}

.review-card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
}

.review-card-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--el-text-color-primary);
}

.review-card-tag {
  flex-shrink: 0;
}

/* Progress steps */
.review-card-progress {
  display: flex;
  align-items: center;
  gap: 0;
  margin-bottom: 12px;
}

.review-step {
  display: flex;
  flex-direction: column;
  align-items: center;
  flex: 1;
  position: relative;
}

.review-step:not(:last-child)::after {
  content: '';
  position: absolute;
  top: 8px;
  left: 50%;
  width: 100%;
  height: 2px;
  background: var(--el-border-color);
  z-index: 0;
}

.review-step.step--done:not(:last-child)::after {
  background: var(--el-color-success);
}

.review-step.step--active:not(:last-child)::after {
  background: var(--el-color-primary-light-5);
}

.review-step-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  z-index: 1;
  font-size: 14px;
}

.step-dot-empty {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  border: 2px solid var(--el-border-color);
  background: var(--el-bg-color);
}

.review-step.step--done .review-step-dot {
  color: var(--el-color-success);
}

.review-step.step--active .review-step-dot {
  color: var(--el-color-primary);
}

.review-step.step--failed .review-step-dot {
  color: var(--el-color-danger);
}

.review-step.step--withdrawn .review-step-dot {
  color: var(--el-text-color-placeholder);
}

.review-step-label {
  font-size: 11px;
  color: var(--el-text-color-secondary);
  margin-top: 4px;
  white-space: nowrap;
}

.review-step.step--done .review-step-label {
  color: var(--el-color-success);
}

.review-step.step--active .review-step-label {
  color: var(--el-color-primary);
  font-weight: 500;
}

.review-step.step--failed .review-step-label {
  color: var(--el-color-danger);
}

/* Meta rows */
.review-card-meta {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 8px;
}

.review-card-meta-item {
  display: flex;
  align-items: flex-start;
  gap: 4px;
  font-size: 11px;
}

.meta-label {
  color: var(--el-text-color-placeholder);
  flex-shrink: 0;
  min-width: 38px;
}

.meta-value {
  color: var(--el-text-color-regular);
  word-break: break-all;
}

.meta-mono {
  font-family: monospace;
  font-size: 10px;
}

/* Rejection reason */
.review-card-reason {
  font-size: 11px;
  color: var(--el-color-danger);
  background: var(--el-color-danger-light-9);
  border-radius: 4px;
  padding: 6px 8px;
  margin-top: 6px;
  line-height: 1.5;
}
.review-card-retry {
  margin-top: 8px;
  display: flex;
  justify-content: flex-end;
}

/* Uploaded file info */
.uploaded-file-info {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  background: var(--el-fill-color-light);
  border-radius: 6px;
  border: 1px solid var(--el-border-color-lighter);
}
.uploaded-file-info .file-icon {
  font-size: 20px;
  color: var(--el-color-primary);
}
.uploaded-file-info .file-name {
  flex: 1;
  font-size: 14px;
  color: var(--el-text-color-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Statistics grid ─────────────────────────────────────────────── */
.stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
  gap: 16px;
}

.stats-card {
  min-height: 320px;
}

.chart-container {
  width: 100%;
  height: 260px;
}
</style>
