<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">上传新版本</el-button>
            <el-button @click="loadAppVersions" :loading="loadingApp">刷新</el-button>
          </div>

          <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-tooltip
                        v-if="item.reason"
                        :content="item.reason"
                        placement="top"
                      >
                        <el-tag
                          :type="reviewTagType(item.state)"
                          size="small"
                          style="margin:2px"
                        >{{ storeLabel(item.store) }} · {{ reviewDisplayLabel(item) }}</el-tag>
                      </el-tooltip>
                      <el-tag
                        v-else
                        :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="220" 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 && 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="version" label="版本" width="100" />
            <el-table-column prop="platform" 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 prop="minCommonVersion" label="最低 Common 版本" width="160" />
            <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">
              <el-alert
                title="这里说明各商店配置项与跳转页的获取方式。App Store 和鸿蒙只需要跳转页；审核通知只配一次，所有市场共享。"
                type="info"
                :closable="false"
                show-icon
                style="margin-bottom:16px"
              />
              <div class="guide-grid">
                <el-card v-for="store in STORE_DEFS" :key="store.type" class="guide-card" shadow="never">
                  <div class="guide-card-title-row">
                    <div>
                      <div class="guide-card-title">{{ store.label }}</div>
                      <div class="guide-card-subtitle">{{ store.guideSubtitle }}</div>
                    </div>
                    <el-tag size="small" type="info">{{ store.shortLabel }}</el-tag>
                  </div>
                  <el-steps direction="vertical" :active="store.guideSteps.length" finish-status="success" style="margin:12px 0 8px">
                    <el-step v-for="step in store.guideSteps" :key="step.title" :title="step.title" :description="step.description" />
                  </el-steps>
                  <el-image
                    v-if="store.guideImage"
                    :src="store.guideImage"
                    fit="cover"
                    class="guide-image"
                    preview-teleported
                  />
                  <div class="guide-hint">{{ store.guideHint }}</div>
                  <el-divider />
                  <div class="guide-link-title">应用跳转链接的获取方式</div>
                  <div class="guide-link-hint">{{ store.jumpLinkHint }}</div>
                  <el-link :href="store.guideUrl" target="_blank" type="primary">查看官方文档</el-link>
                </el-card>
              </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-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.graySelectCallbackUrl" placeholder="选择成员时调用的回调地址" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
              </el-form-item>
              <el-form-item label="成员选择密钥">
                <el-input v-model="publishConfigForm.graySelectCallbackSecret" type="password" show-password placeholder="可选，用于成员选择回调验签" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
              </el-form-item>
              <el-form-item label="成员目录同步回调">
                <el-input v-model="publishConfigForm.grayDirectorySyncCallbackUrl" placeholder="同步所有成员时调用的回调地址" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
              </el-form-item>
              <el-form-item label="成员目录密钥">
                <el-input v-model="publishConfigForm.grayDirectorySyncCallbackSecret" type="password" show-password placeholder="可选，用于成员同步回调验签" :disabled="publishConfigForm.allowAnonymousUpdateCheck" />
              </el-form-item>
              <el-form-item label="成员选择方式">
                <el-radio-group v-model="publishConfigForm.graySelectionSource" :disabled="publishConfigForm.allowAnonymousUpdateCheck">
                  <el-radio-button value="LOCAL" :disabled="!hasGrayDirectorySyncCallback">同步后本地选择</el-radio-button>
                  <el-radio-button value="CALLBACK" :disabled="!hasGraySelectCallback">回调直接返回成员</el-radio-button>
                </el-radio-group>
              </el-form-item>
              <el-form-item>
                <el-button @click="syncGrayMembers" :loading="loadingGrayMembers" :disabled="!hasGrayDirectorySyncCallback || publishConfigForm.allowAnonymousUpdateCheck">同步成员</el-button>
              </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>
      </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="dialogWidth">
      <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-else-if="grayForm.enabled && grayForm.grayMode === 'MEMBERS'">
          <el-form-item label="成员来源">
            <el-radio-group v-model="grayForm.selectionSource">
              <el-radio-button value="LOCAL">同步后本地选择</el-radio-button>
              <el-radio-button value="CALLBACK">回调直接返回成员</el-radio-button>
            </el-radio-group>
          </el-form-item>
          <el-form-item label="成员搜索" v-if="grayForm.selectionSource === 'LOCAL'">
            <el-input
              v-model="grayMemberKeyword"
              placeholder="搜索 userId / name / 分组"
              clearable
              style="width: 320px"
              @change="loadGrayMembers"
            />
            <el-button style="margin-left: 8px" @click="syncGrayMembers" :loading="loadingGrayMembers">同步成员</el-button>
          </el-form-item>
          <el-alert
            v-if="grayForm.selectionSource === 'CALLBACK'"
            type="info"
            :closable="false"
            show-icon
            title="选择成员时将调用发布配置里的成员选择回调，由你的系统直接返回可参与灰度的成员列表。"
          />
          <div v-if="grayForm.selectionSource === 'LOCAL'" class="gray-member-groups">
            <el-empty v-if="!grayMembers.length" description="暂无成员数据，请先同步" />
            <el-collapse v-else>
              <el-collapse-item v-for="group in grayMembers" :key="group.groupName" :name="group.groupName">
                <template #title>
                  <div class="gray-group-title">
                    <span>{{ group.groupName }}</span>
                    <el-tag size="small">{{ group.members.length }}</el-tag>
                  </div>
                </template>
                <el-checkbox-group v-model="grayMemberIds">
                  <div v-for="member in group.members" :key="member.userId" class="gray-member-row">
                    <el-checkbox :value="member.userId">
                      <span class="gray-member-id">{{ member.userId }}</span>
                      <span class="gray-member-name">{{ member.name || '未命名' }}</span>
                    </el-checkbox>
                  </div>
                </el-checkbox-group>
              </el-collapse-item>
            </el-collapse>
          </div>
        </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="MANUAL">手动上架</el-radio-button>
              <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"
            >审核进行中，将跳过</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>

    <!-- Preflight Dialog -->
    <el-dialog v-model="showPreflight" title="提审前检查" :width="isMobile ? '100%' : '560px'">
      <div v-if="preflightLoading" v-loading="true" style="min-height:120px" />
      <div v-else-if="preflightResult">
        <p>版本 <strong>{{ preflightResult.versionName }}</strong> 的前置检查结果：</p>
        <el-table :data="preflightResult.stores" border size="small" style="margin-top:12px">
          <el-table-column prop="storeType" label="厂商" width="100">
            <template #default="{row}">{{ storeLabel(row.storeType) }}</template>
          </el-table-column>
          <el-table-column label="线上版本" width="120">
            <template #default="{row}">
              <span v-if="row.onlineVersionCode">{{ row.onlineVersionName || row.onlineVersionCode }}</span>
              <span v-else class="text-muted">—</span>
            </template>
          </el-table-column>
          <el-table-column label="状态" width="160">
            <template #default="{row}">
              <el-tag :type="preflightTagType(row.reviewState)" size="small">
                {{ row.nonCurrentRelease ? '已上线（非本次发布）' : preflightStateLabel(row.reviewState) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="是否允许" width="90">
            <template #default="{row}">
              <el-tag :type="row.canSubmit ? 'success' : 'danger'" size="small">
                {{ row.canSubmit ? '允许' : '阻止' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="原因" min-width="140">
            <template #default="{row}">
              <span v-if="row.blockReason" class="text-danger">{{ row.blockReason }}</span>
              <span v-else class="text-muted">—</span>
            </template>
          </el-table-column>
        </el-table>
        <el-checkbox v-model="preflightSkipCheck" style="margin-top:12px" v-if="preflightResult.stores.some(s => !s.canSubmit)">
          跳过厂商状态检查后继续提交（管理员）
        </el-checkbox>
      </div>
      <template #footer>
        <el-button @click="showPreflight = false">取消</el-button>
        <el-button
          type="primary"
          :disabled="preflightResult ? !preflightResult.stores.every(s => s.canSubmit) && !preflightSkipCheck : true"
          @click="showPreflight = false; showSubmitStore = true"
        >继续提交</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
            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>
        </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=".bundle,.js"
          >
            <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
            <div class="el-upload__text">
              将 RN Bundle 拖到这里，或 <em>点击选择文件</em>
            </div>
            <template #tip>
              <div class="el-upload__tip">
                选择或拖入文件后会自动识别模块、平台、版本和 Common 版本。
              </div>
            </template>
          </el-upload>
        </el-form-item>
        <el-form-item v-if="rnInspectUploadProgress > 0" label="识别进度">
          <div class="upload-progress-block">
            <el-progress :percentage="rnInspectUploadProgress" :status="rnInspectUploadProgress === 100 ? 'success' : undefined" />
            <span class="upload-progress-text">{{ rnInspectUploadProgress }}%</span>
          </div>
        </el-form-item>
        <el-alert
          type="info"
          :closable="false"
          show-icon
          title="推荐文件名格式：moduleId__ANDROID__1.0.0__1.0.0__com.example.app.bundle，系统会按命名自动识别模块、平台、版本、最低 Common 版本和包名。"
        />
        <el-form-item label="模块ID"><el-input v-model="rnUploadForm.moduleId" placeholder="可由文件名自动识别" /></el-form-item>
        <el-form-item label="平台">
          <el-select v-model="rnUploadForm.platform">
            <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="版本"><el-input v-model="rnUploadForm.version" placeholder="可由文件名自动识别" /></el-form-item>
        <el-form-item label="最低 Common 版本"><el-input v-model="rnUploadForm.minCommonVersion" placeholder="可由文件名自动识别" /></el-form-item>
        <el-form-item label="包名 / Bundle">
          <el-input v-model="rnUploadForm.packageName" placeholder="可选，建议与应用包名一致" />
        </el-form-item>
        <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、.bundle、.js</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, Edit, Loading, UploadFilled, WarningFilled } from '@element-plus/icons-vue'
import { appApi, type App } from '@/api/app'
import { fileApi } from '@/api/file'
import {
  updateAdminApi,
  type AppPackageInspectResult,
  type AppVersion,
  type GrayMemberGroup,
  type GrayMode,
  type GraySelectionSource,
  type PreflightSubmitResultDto,
  type PublishMode,
  type RnBundle,
  type RnBundleInspectResult,
  type StoreConfig,
  type StoreType,
} from '@/api/update'
import {
  connectStoreReviewRealtime,
  disconnectStoreReviewRealtime,
  notifyStoreReviewRefresh,
  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 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 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 (ext === '.bundle' || ext === '.js') {
    showUploadRn.value = true
    await nextTick()
    await onRnBundleChange({ raw: file })
  } else {
    ElMessage.warning(`不支持的文件类型：${file.name}，请拖入 .apk、.bundle 或 .js 文件`)
  }
}

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,
  graySelectionSource: 'LOCAL' as GraySelectionSource,
  graySelectCallbackUrl: '',
  graySelectCallbackSecret: '',
  grayDirectorySyncCallbackUrl: '',
  grayDirectorySyncCallbackSecret: '',
  parallelStoreUpload: true,
})
const grayMembers = ref<GrayMemberGroup[]>([])
const loadingGrayMembers = ref(false)
const grayMemberKeyword = ref('')
const grayMemberGroupFilter = ref('')
const grayMemberIds = ref<string[]>([])
const appPackageInspecting = ref(false)
const appPackageUploadProgress = ref(0)
const appVersionUploadProgress = ref(0)
const rnInspectUploadProgress = ref(0)
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)
let storeReviewReloadTimer: ReturnType<typeof setTimeout> | null = null
let storeReviewAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
let storeReviewDialogPollTimer: ReturnType<typeof setInterval> | null = null

const hasGraySelectCallback = computed(() => Boolean(publishConfigForm.value.graySelectCallbackUrl.trim()))
const hasGrayDirectorySyncCallback = computed(() => Boolean(publishConfigForm.value.grayDirectorySyncCallbackUrl.trim()))
const hasAnyGrayCallback = computed(() => hasGraySelectCallback.value || hasGrayDirectorySyncCallback.value)
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: [
      marketUrlField('App Store 详情页链接'),
    ],
    guideSubtitle: 'App Store 只保留应用商店跳转链接',
    guideUrl: 'https://developer.apple.com/app-store/',
    guideSteps: [
      { title: '进入 App Store Connect', description: '打开对应应用的详情页或 App Store 公开页面。' },
      { title: '复制 App Store 页面链接', description: '这里只需要保存跳转页，不需要再填密钥。' },
      { title: '保存配置', description: '配置完成后可在版本提醒中直接跳转。' },
    ],
    jumpLinkHint: 'App Store 链接可选填写，通常以 apps.apple.com 开头，需要跳转时再补。',
    guideHint: '不再配置 Team ID、Key ID 或私钥；发布侧只在需要跳转时使用该链接。',
  },
  {
    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: [
      marketUrlField('鸿蒙应用市场详情页链接'),
    ],
    guideSubtitle: '鸿蒙应用只保留市场跳转页',
    guideUrl: 'https://developer.huawei.com/consumer/cn/',
    guideSteps: [
      { title: '打开鸿蒙应用市场', description: '确认目标应用已在鸿蒙市场上架或待上架。' },
      { title: '复制详情页链接', description: '填写市场跳转页面，便于版本提醒直接跳转。' },
      { title: '保存配置', description: '配置完成后可在版本提醒里引用。' },
    ],
    jumpLinkHint: '鸿蒙应用市场详情页链接可选填写，通常使用 appgallery.huawei.com/app/detail?id=包名。',
    guideHint: '这里只保存鸿蒙应用市场的独立跳转页，不参与 Android 审核提交。',
  },
  {
    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: '该配置对所有市场共用，不再按渠道重复配置。',
  },
]

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) {
  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 normalizeCallbackUrl = (value: unknown) => {
    const text = String(value ?? '').trim()
    return /^https?:\/\//i.test(text) ? text : ''
  }
  const grayMode = String(raw?.grayMode ?? 'PERCENT') as GrayMode
  const graySelectionSource = String(raw?.graySelectionSource ?? 'LOCAL') as GraySelectionSource
  return {
    allowAnonymousUpdateCheck: Boolean(raw?.allowAnonymousUpdateCheck ?? raw?.defaultAllowAnonymousUpdateCheck ?? false),
    defaultGrayPercent: Number((raw as Record<string, unknown>)?.defaultGrayPercent ?? 0),
    grayMode,
    graySelectionSource,
    graySelectCallbackUrl: normalizeCallbackUrl((raw as Record<string, unknown>)?.graySelectCallbackUrl),
    graySelectCallbackSecret: String((raw as Record<string, unknown>)?.graySelectCallbackSecret ?? ''),
    grayDirectorySyncCallbackUrl: normalizeCallbackUrl((raw as Record<string, unknown>)?.grayDirectorySyncCallbackUrl),
    grayDirectorySyncCallbackSecret: String((raw as Record<string, unknown>)?.grayDirectorySyncCallbackSecret ?? ''),
    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'
      publishConfigForm.value.graySelectionSource = 'LOCAL'
    }
    if (publishConfigForm.value.grayMode === 'MEMBERS' && !hasAnyGrayCallback.value) {
      publishConfigForm.value.grayMode = 'PERCENT'
    }
  } catch {
    publishConfigForm.value = normalizePublishConfig({})
  } finally {
    loadingPublishConfig.value = false
  }
}

async function savePublishConfig() {
  savingPublishConfig.value = true
  try {
    const normalizeCallbackUrl = (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),
      graySelectCallbackUrl: normalizeCallbackUrl(publishConfigForm.value.graySelectCallbackUrl),
      grayDirectorySyncCallbackUrl: normalizeCallbackUrl(publishConfigForm.value.grayDirectorySyncCallbackUrl),
    }
    if (payload.allowAnonymousUpdateCheck) {
      payload.grayMode = 'PERCENT'
      payload.graySelectionSource = 'LOCAL'
    }
    if (payload.grayMode === 'MEMBERS' && !hasAnyGrayCallback.value) {
      ElMessage.warning('成员模式至少需要配置一个回调地址')
      return
    }
    if (payload.grayMode === 'MEMBERS' && !hasGrayDirectorySyncCallback.value) {
      payload.graySelectionSource = 'CALLBACK'
    }
    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>('MANUAL')
const submitStoreScheduledAt = ref('')

// Preflight dialog state
const showPreflight = ref(false)
const preflightLoading = ref(false)
const preflightResult = ref<PreflightSubmitResultDto | null>(null)
const preflightSkipCheck = ref(false)

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

async function openSubmitStoreDialog(row: AppVersion) {
  submitStoreVersion.value = row
  preflightResult.value = null
  preflightSkipCheck.value = false
  showPreflight.value = true
  preflightLoading.value = true
  try {
    const res = await updateAdminApi.preflightStoreSubmission(row.id)
    preflightResult.value = res.data.data
    // Auto-open submit dialog if all allowed
    const allAllowed = preflightResult.value?.stores.every(s => s.canSubmit) ?? false
    if (allAllowed) {
      showPreflight.value = false
      showSubmitStore.value = true
    }
  } catch (e: any) {
    ElMessage.error(e?.response?.data?.message || '前置检查失败')
  } finally {
    preflightLoading.value = false
  }
  // Fallback: pre-select stores
  selectedStores.value = enabledStores.value
    .map(s => s.type)
    .filter(t => !isStoreActiveReview(row, t))
  submitStoreMode.value = row.storeSubmitMode ?? 'MANUAL'
  submitStoreScheduledAt.value = row.storeSubmitScheduledAt ?? ''
}

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

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

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 grayForm = ref({
  enabled: true,
  grayMode: 'PERCENT' as GrayMode,
  percent: 10,
  selectionSource: 'LOCAL' as GraySelectionSource,
})

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: hasAnyGrayCallback.value ? publishConfigForm.value.grayMode : 'PERCENT',
    percent: publishConfigForm.value.defaultGrayPercent || 10,
    selectionSource: hasGrayDirectorySyncCallback.value
      ? publishConfigForm.value.graySelectionSource
      : (hasGraySelectCallback.value ? 'CALLBACK' : 'LOCAL'),
  }
  grayMemberIds.value = []
  grayMemberKeyword.value = ''
  grayMemberGroupFilter.value = ''
  grayMembers.value = []
  showGray.value = true
  if (grayForm.value.selectionSource === 'LOCAL' && hasGrayDirectorySyncCallback.value) {
    loadGrayMembers()
  }
}

async function loadGrayMembers() {
  if (!showGray.value || !hasGrayDirectorySyncCallback.value) return
  loadingGrayMembers.value = true
  try {
    const res = await updateAdminApi.listGrayMembers(appKey.value, grayMemberKeyword.value || undefined, grayMemberGroupFilter.value || undefined)
    grayMembers.value = res.data.data
  } catch {
    grayMembers.value = []
  } finally {
    loadingGrayMembers.value = false
  }
}

async function syncGrayMembers() {
  if (!hasGrayDirectorySyncCallback.value) {
    ElMessage.warning('未配置成员目录同步回调，无法同步成员')
    return
  }
  loadingGrayMembers.value = true
  try {
    const res = await updateAdminApi.syncGrayMembers(appKey.value)
    grayMembers.value = res.data.data
    ElMessage.success('成员已同步')
  } catch {
    ElMessage.error('同步失败')
  } finally {
    loadingGrayMembers.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.selectionSource === 'LOCAL' && !hasGrayDirectorySyncCallback.value) {
    ElMessage.warning('未配置成员目录同步回调，无法选择本地成员')
    return
  }
  if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'CALLBACK' && !hasGraySelectCallback.value) {
    ElMessage.warning('未配置成员选择回调，无法使用回调成员选择')
    return
  }
  if (grayForm.value.enabled && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'LOCAL' && !grayMemberIds.value.length) {
    ElMessage.warning('请选择至少一个灰度成员')
    return
  }
  submittingGray.value = true
  try {
    const { id, type } = grayTarget.value
    const payload: {
      enabled: boolean
      grayMode: GrayMode
      percent?: number
      memberIds?: string[]
      selectionSource?: GraySelectionSource
    } = {
      enabled: grayForm.value.enabled,
      grayMode: grayForm.value.grayMode,
      selectionSource: grayForm.value.selectionSource,
    }
    if (grayForm.value.grayMode === 'PERCENT') {
      payload.percent = grayForm.value.percent
    } else {
      payload.memberIds = grayMemberIds.value
    }
    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: '',
})

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 = ''
  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 = ''
  appPackageUploadProgress.value = 0
  appPackageInspecting.value = false
}

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

  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
    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 = ''
        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
  }
}

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)
    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({
  moduleId: '',
  platform: 'ANDROID' as 'ANDROID' | 'IOS' | 'HARMONY',
  version: '',
  minCommonVersion: '',
  packageName: '',
  note: '',
  file: null as File | null,
})

function parseRnBundleName(fileName: string): RnBundleInspectResult | null {
  const baseName = fileName.replace(/\.[^.]+$/, '')
  const parts = baseName.split('__')
  if (parts.length >= 4) {
    return {
      moduleId: parts[0],
      platform: parts[1].toUpperCase() as 'ANDROID' | 'IOS' | 'HARMONY',
      version: parts[2],
      minCommonVersion: parts[3],
      packageName: parts[4],
      fileName,
      detected: true,
    }
  }
  return null
}

async function onRnBundleChange(uploadFile: { raw?: File } | null) {
  const file = uploadFile?.raw ?? null
  rnUploadForm.value.file = file
  if (!file) return

  const local = parseRnBundleName(file.name)
  if (local) {
    if (local.moduleId) rnUploadForm.value.moduleId = local.moduleId
    if (local.platform) rnUploadForm.value.platform = local.platform
    if (local.version) rnUploadForm.value.version = local.version
    if (local.minCommonVersion) rnUploadForm.value.minCommonVersion = local.minCommonVersion
    if (local.packageName) rnUploadForm.value.packageName = local.packageName
    return
  }

  const formData = new FormData()
  formData.append('bundle', file)
  rnInspectUploadProgress.value = 0
  try {
    const res = await updateAdminApi.inspectRnBundle(formData, (percent) => {
      rnInspectUploadProgress.value = percent
    })
    rnInspectUploadProgress.value = 100
    const inspected = res.data.data as RnBundleInspectResult
    if (inspected.moduleId) rnUploadForm.value.moduleId = inspected.moduleId
    if (inspected.platform) rnUploadForm.value.platform = inspected.platform
    if (inspected.version) rnUploadForm.value.version = inspected.version
    if (inspected.minCommonVersion) rnUploadForm.value.minCommonVersion = inspected.minCommonVersion
    if (inspected.packageName) rnUploadForm.value.packageName = inspected.packageName
  } catch {
    ElMessage.warning('已选择文件，但未能从文件名识别出 RN Bundle 元数据，请补全后上传')
  } finally {
    rnInspectUploadProgress.value = 0
  }
}

async function submitRnUpload() {
  const f = rnUploadForm.value
  if (!f.moduleId || !f.version || !f.file) return ElMessage.warning('请填写模块ID、版本和 Bundle 文件')
  uploadingRn.value = true
  rnBundleUploadProgress.value = 0
  try {
    const fd = new FormData()
    fd.append('appKey', appKey.value)
    fd.append('moduleId', f.moduleId)
    fd.append('platform', f.platform)
    fd.append('version', f.version)
    if (f.minCommonVersion) fd.append('minCommonVersion', f.minCommonVersion)
    if (f.packageName) fd.append('packageName', f.packageName)
    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')
}

function preflightTagType(state: string): string {
  return {
    ONLINE: 'success',
    UNDER_REVIEW: 'warning',
    UNDER_REVIEW_XIAOMI: 'warning',
    REJECTED: 'danger',
    NOT_FOUND: 'info',
    UNKNOWN: 'info',
    QUERY_FAILED: 'danger',
  }[state] ?? ''
}

function preflightStateLabel(state: string): string {
  return {
    ONLINE: '已上线',
    UNDER_REVIEW: '审核中',
    UNDER_REVIEW_XIAOMI: '审核中(版本号可能不准确)',
    REJECTED: '已拒绝',
    NOT_FOUND: '未找到',
    UNKNOWN: '未知',
    QUERY_FAILED: '查询失败',
  }[state] ?? state
}

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 formatTime(t: string) {
  return t ? new Date(t).toLocaleString('zh-CN') : '-'
}

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

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(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([grayMemberKeyword, grayMemberGroupFilter], () => {
  if (showGray.value && grayForm.value.grayMode === 'MEMBERS' && grayForm.value.selectionSource === 'LOCAL') {
    loadGrayMembers()
  }
})

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

onMounted(() => {
  updateViewport()
  window.addEventListener('resize', updateViewport)
  if (isServicesPortal.value) {
    appApi.list().then(res => { portalApps.value = res.data.data })
    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)
  disconnectStoreReviewRealtime()
  if (storeReviewReloadTimer) {
    clearTimeout(storeReviewReloadTimer)
    storeReviewReloadTimer = null
  }
  if (storeReviewAutoRefreshTimer) {
    clearInterval(storeReviewAutoRefreshTimer)
    storeReviewAutoRefreshTimer = null
  }
  stopDialogPoll()
})
</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-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 16px;
}

.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-member-groups {
  width: 100%;
}

.gray-group-title {
  display: flex;
  align-items: center;
  gap: 8px;
}

.gray-member-row {
  padding: 6px 0;
}

.gray-member-id {
  font-weight: 600;
  margin-right: 8px;
}

.gray-member-name {
  color: var(--el-text-color-secondary);
}

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